Skip to content

Commit 466634a

Browse files
pkg/webhook: test ResourceInterpreterCustomization
In this commit, we introduce unit tests for `ValidationAdmission` webhook for the `ResourceInterpreterCustomization` resource. The tests include: - Tests how the webhook responds when decoding the request object fails. - Tests how the webhook responds when it fails to retrieve list of objects from a namespace. - Ensures that invalid `ResourceInterpreterCustomization` spec denies the admission. - Confirms that valid `ResourceInterpreterCustomization` spec are allowed through without errors. Signed-off-by: Mohamed Awnallah <[email protected]>
1 parent 4c8bcd4 commit 466634a

File tree

1 file changed

+195
-0
lines changed

1 file changed

+195
-0
lines changed
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
/*
2+
Copyright 2024 The Karmada Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package resourceinterpretercustomization
18+
19+
import (
20+
"context"
21+
"errors"
22+
"net/http"
23+
"reflect"
24+
"strings"
25+
"testing"
26+
27+
"k8s.io/apimachinery/pkg/runtime"
28+
"sigs.k8s.io/controller-runtime/pkg/client"
29+
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
30+
31+
configv1alpha1 "github.com/karmada-io/karmada/pkg/apis/config/v1alpha1"
32+
)
33+
34+
// ResponseType represents the type of admission response.
35+
type ResponseType string
36+
37+
const (
38+
Denied ResponseType = "Denied"
39+
Allowed ResponseType = "Allowed"
40+
Errored ResponseType = "Errored"
41+
)
42+
43+
// TestResponse is used to define expected response in a test case.
44+
type TestResponse struct {
45+
Type ResponseType
46+
Message string
47+
}
48+
49+
type fakeValidationDecoder struct {
50+
err error
51+
obj runtime.Object
52+
}
53+
54+
// Decode mocks the Decode method of admission.Decoder.
55+
func (f *fakeValidationDecoder) Decode(_ admission.Request, obj runtime.Object) error {
56+
if f.err != nil {
57+
return f.err
58+
}
59+
if f.obj != nil {
60+
reflect.ValueOf(obj).Elem().Set(reflect.ValueOf(f.obj).Elem())
61+
}
62+
return nil
63+
}
64+
65+
// DecodeRaw mocks the DecodeRaw method of admission.Decoder.
66+
func (f *fakeValidationDecoder) DecodeRaw(_ runtime.RawExtension, obj runtime.Object) error {
67+
if f.err != nil {
68+
return f.err
69+
}
70+
if f.obj != nil {
71+
reflect.ValueOf(obj).Elem().Set(reflect.ValueOf(f.obj).Elem())
72+
}
73+
return nil
74+
}
75+
76+
// fakeClient is a mock implementation of the client.Client interface for testing.
77+
type fakeClient struct {
78+
client.Client
79+
listError error
80+
}
81+
82+
func (f *fakeClient) List(_ context.Context, _ client.ObjectList, _ ...client.ListOption) error {
83+
if f.listError != nil {
84+
return f.listError
85+
}
86+
return nil
87+
}
88+
89+
func TestValidatingAdmission_Handle(t *testing.T) {
90+
tests := []struct {
91+
name string
92+
decoder admission.Decoder
93+
req admission.Request
94+
want TestResponse
95+
listError error
96+
}{
97+
{
98+
name: "Handle_DecodeError_DeniesAdmission",
99+
decoder: &fakeValidationDecoder{
100+
err: errors.New("decode error"),
101+
},
102+
req: admission.Request{},
103+
want: TestResponse{
104+
Type: Errored,
105+
Message: "decode error",
106+
},
107+
},
108+
{
109+
name: "Handle_ListError_InternalError",
110+
decoder: &fakeValidationDecoder{
111+
obj: &configv1alpha1.ResourceInterpreterCustomization{},
112+
},
113+
req: admission.Request{},
114+
listError: errors.New("list error"),
115+
want: TestResponse{
116+
Type: Errored,
117+
Message: "list error",
118+
},
119+
},
120+
{
121+
name: "Handle_WrongLuaCustomizationRetentionScript_DeniesAdmission",
122+
decoder: &fakeValidationDecoder{
123+
obj: &configv1alpha1.ResourceInterpreterCustomization{
124+
Spec: configv1alpha1.ResourceInterpreterCustomizationSpec{
125+
Customizations: configv1alpha1.CustomizationRules{
126+
Retention: &configv1alpha1.LocalValueRetention{LuaScript: `function Retain(desiredObj, observedObj)`},
127+
},
128+
},
129+
},
130+
},
131+
req: admission.Request{},
132+
want: TestResponse{
133+
Type: Denied,
134+
Message: "Lua script error: <string> at EOF",
135+
},
136+
},
137+
{
138+
name: "Handle_ValidRequest_AllowsAdmission",
139+
decoder: &fakeValidationDecoder{
140+
obj: &configv1alpha1.ResourceInterpreterCustomization{
141+
Spec: configv1alpha1.ResourceInterpreterCustomizationSpec{
142+
Target: configv1alpha1.CustomizationTarget{
143+
APIVersion: "foo/v1",
144+
Kind: "bar",
145+
},
146+
},
147+
},
148+
},
149+
req: admission.Request{},
150+
want: TestResponse{
151+
Type: Allowed,
152+
Message: "",
153+
},
154+
},
155+
}
156+
157+
for _, tt := range tests {
158+
t.Run(tt.name, func(t *testing.T) {
159+
v := &ValidatingAdmission{
160+
Client: &fakeClient{listError: tt.listError},
161+
Decoder: tt.decoder,
162+
}
163+
got := v.Handle(context.Background(), tt.req)
164+
165+
// Extract type and message from the actual response.
166+
gotType := extractResponseType(got)
167+
gotMessage := extractErrorMessage(got)
168+
169+
if gotType != tt.want.Type || !strings.Contains(gotMessage, tt.want.Message) {
170+
t.Errorf("Handle() = {Type: %v, Message: %v}, want {Type: %v, Message: %v}", gotType, gotMessage, tt.want.Type, tt.want.Message)
171+
}
172+
})
173+
}
174+
}
175+
176+
// extractResponseType extracts the type of admission response.
177+
func extractResponseType(resp admission.Response) ResponseType {
178+
if resp.Allowed {
179+
return Allowed
180+
}
181+
if resp.Result != nil {
182+
if resp.Result.Code == http.StatusBadRequest || resp.Result.Code == http.StatusInternalServerError {
183+
return Errored
184+
}
185+
}
186+
return Denied
187+
}
188+
189+
// extractErrorMessage extracts the error message from a Denied/Errored response.
190+
func extractErrorMessage(resp admission.Response) string {
191+
if !resp.Allowed && resp.Result != nil {
192+
return resp.Result.Message
193+
}
194+
return ""
195+
}

0 commit comments

Comments
 (0)