Merge pull request #5542 from mohamedawnallah/unitTestInterpreter

pkg/webhook: unit test Interpreter
This commit is contained in:
karmada-bot 2024-10-09 11:52:20 +08:00 committed by GitHub
commit 376d20376c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 1030 additions and 1 deletions

View File

@ -43,7 +43,7 @@ func NewDecoder(scheme *runtime.Scheme) *Decoder {
// It errors out if req.Object.Raw is empty i.e. containing 0 raw bytes.
func (d *Decoder) Decode(req Request, into runtime.Object) error {
if len(req.Object.Raw) == 0 {
return fmt.Errorf("there is no context to decode")
return fmt.Errorf("there is no content to decode")
}
return d.DecodeRaw(req.Object, into)
}

View File

@ -0,0 +1,277 @@
/*
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 interpreter
import (
"fmt"
"strings"
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
schema "k8s.io/apimachinery/pkg/runtime/schema"
configv1alpha1 "github.com/karmada-io/karmada/pkg/apis/config/v1alpha1"
)
type Interface interface {
GetAPIVersion() string
GetKind() string
GetName() string
}
// MyTestPod represents a simplified version of a Kubernetes Pod for testing purposes.
// It includes basic fields such as API version, kind, and metadata.
type MyTestPod struct {
APIVersion string `json:"apiVersion"`
Kind string `json:"kind"`
Metadata struct {
Name string `json:"name"`
} `json:"metadata"`
}
// DeepCopyObject creates a deep copy of the MyTestPod instance.
// This method is part of the runtime.Object interface and ensures that modifications
// to the copy do not affect the original object.
func (p *MyTestPod) DeepCopyObject() runtime.Object {
return &MyTestPod{
APIVersion: p.APIVersion,
Kind: p.Kind,
Metadata: p.Metadata,
}
}
// GetObjectKind returns the schema.ObjectKind for the MyTestPod instance.
// This method is part of the runtime.Object interface and provides the API version
// and kind of the object, which is used for object identification in Kubernetes.
func (p *MyTestPod) GetObjectKind() schema.ObjectKind {
return &metav1.TypeMeta{
APIVersion: p.APIVersion,
Kind: p.Kind,
}
}
// GetAPIVersion returns the API version of the MyTestPod.
func (p *MyTestPod) GetAPIVersion() string {
return p.APIVersion
}
// GetKind returns the kind of the MyTestPod.
func (p *MyTestPod) GetKind() string {
return p.Kind
}
// GetName returns the name of the MyTestPod.
func (p *MyTestPod) GetName() string {
return p.Metadata.Name
}
func TestNewDecoder(t *testing.T) {
tests := []struct {
name string
}{
{
name: "NewDecoder_ValidDecoder_DecoderIsValid",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
scheme := runtime.NewScheme()
decoder := NewDecoder(scheme)
if decoder == nil {
t.Errorf("expected decoder to not be nil")
}
})
}
}
func TestDecodeRaw(t *testing.T) {
tests := []struct {
name string
apiVersion string
kind string
objName string
rawObj *runtime.RawExtension
into Interface
prep func(re *runtime.RawExtension, apiVersion, kind, name string) error
verify func(into Interface, apiVersion, kind, name string) error
wantErr bool
errMsg string
}{
{
name: "DecodeRaw_ValidRaw_DecodeRawIsSuccessful",
objName: "test-pod",
kind: "Pod",
apiVersion: "v1",
rawObj: &runtime.RawExtension{
Raw: []byte{},
},
into: &unstructured.Unstructured{},
prep: func(re *runtime.RawExtension, apiVersion, kind, name string) error {
re.Raw = []byte(fmt.Sprintf(`{"apiVersion": "%s", "kind": "%s", "metadata": {"name": "%s"}}`, apiVersion, kind, name))
return nil
},
verify: verifyRuntimeObject,
wantErr: false,
},
{
name: "DecodeRaw_IntoNonUnstructuredType_RawDecoded",
objName: "test-pod",
kind: "Pod",
apiVersion: "v1",
rawObj: &runtime.RawExtension{
Raw: []byte{},
},
into: &MyTestPod{},
prep: func(re *runtime.RawExtension, apiVersion, kind, name string) error {
re.Raw = []byte(fmt.Sprintf(`{"apiVersion": "%s", "kind": "%s", "metadata": {"name": "%s"}}`, apiVersion, kind, name))
return nil
},
verify: verifyRuntimeObject,
wantErr: false,
},
{
name: "DecodeRaw_EmptyRaw_NoContentToDecode",
rawObj: &runtime.RawExtension{
Raw: []byte{},
},
into: &unstructured.Unstructured{},
prep: func(*runtime.RawExtension, string, string, string) error { return nil },
verify: func(Interface, string, string, string) error { return nil },
wantErr: true,
errMsg: "there is no content to decode",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if err := test.prep(test.rawObj, test.apiVersion, test.kind, test.objName); err != nil {
t.Errorf("failed to prep the runtime raw extension object: %v", err)
}
scheme := runtime.NewScheme()
decoder := NewDecoder(scheme)
intoObj, ok := test.into.(runtime.Object)
if !ok {
t.Errorf("failed to type assert into object into runtime rawextension")
}
err := decoder.DecodeRaw(*test.rawObj, intoObj)
if err != nil && !test.wantErr {
t.Errorf("unexpected error while decoding the raw: %v", err)
}
if err == nil && test.wantErr {
t.Errorf("expected an error, but got none")
}
if err != nil && test.wantErr && !strings.Contains(err.Error(), test.errMsg) {
t.Errorf("expected %s error msg to contain %s", err.Error(), test.errMsg)
}
if err := test.verify(test.into, test.apiVersion, test.kind, test.objName); err != nil {
t.Errorf("failed to verify decoding the raw: %v", err)
}
})
}
}
func TestDecode(t *testing.T) {
tests := []struct {
name string
apiVersion string
kind string
objName string
req *Request
into Interface
prep func(re *Request, apiVersion, kind, name string) error
verify func(into Interface, apiVersion, kind, name string) error
wantErr bool
errMsg string
}{
{
name: "Decode_ValidRequest_DecodeRequestIsSuccessful",
objName: "test-pod",
kind: "Pod",
apiVersion: "v1",
req: &Request{
ResourceInterpreterRequest: configv1alpha1.ResourceInterpreterRequest{
Object: runtime.RawExtension{},
},
},
into: &unstructured.Unstructured{},
prep: func(re *Request, apiVersion, kind, name string) error {
re.ResourceInterpreterRequest.Object.Raw = []byte(fmt.Sprintf(`{"apiVersion": "%s", "kind": "%s", "metadata": {"name": "%s"}}`, apiVersion, kind, name))
return nil
},
verify: verifyRuntimeObject,
wantErr: false,
},
{
name: "Decode_EmptyRaw_NoContentToDecode",
req: &Request{
ResourceInterpreterRequest: configv1alpha1.ResourceInterpreterRequest{
Object: runtime.RawExtension{},
},
},
into: &unstructured.Unstructured{},
prep: func(*Request, string, string, string) error { return nil },
verify: func(Interface, string, string, string) error { return nil },
wantErr: true,
errMsg: "there is no content to decode",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if err := test.prep(test.req, test.apiVersion, test.kind, test.objName); err != nil {
t.Errorf("failed to prep the runtime raw extension object: %v", err)
}
scheme := runtime.NewScheme()
decoder := NewDecoder(scheme)
if decoder == nil {
t.Errorf("expected decoder to not be nil")
}
intoObj, ok := test.into.(runtime.Object)
if !ok {
t.Errorf("failed to type assert into object into runtime rawextension")
}
err := decoder.Decode(*test.req, intoObj)
if err != nil && !test.wantErr {
t.Errorf("unexpected error while decoding the raw: %v", err)
}
if err == nil && test.wantErr {
t.Errorf("expected an error, but got none")
}
if err != nil && test.wantErr && !strings.Contains(err.Error(), test.errMsg) {
t.Errorf("expected %s error msg to contain %s", err.Error(), test.errMsg)
}
if err := test.verify(test.into, test.apiVersion, test.kind, test.objName); err != nil {
t.Errorf("failed to verify decoding the raw: %v", err)
}
})
}
}
// verifyRuntimeObject checks if the runtime object (`into`) matches the given
// `apiVersion`, `kind`, and `name`. It returns an error if any field doesn't match.
func verifyRuntimeObject(into Interface, apiVersion, kind, name string) error {
if got := into.GetAPIVersion(); got != apiVersion {
return fmt.Errorf("expected API version '%s', got '%s'", apiVersion, got)
}
if got := into.GetKind(); got != kind {
return fmt.Errorf("expected kind '%s', got '%s'", kind, got)
}
if got := into.GetName(); got != name {
return fmt.Errorf("expected name '%s', got '%s'", name, got)
}
return nil
}

View File

@ -0,0 +1,346 @@
/*
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 interpreter
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"k8s.io/apimachinery/pkg/util/json"
configv1alpha1 "github.com/karmada-io/karmada/pkg/apis/config/v1alpha1"
)
// HTTPMockHandler implements the Handler and DecoderInjector interfaces for testing.
type HTTPMockHandler struct {
response Response
decoder *Decoder
}
// Handle implements the Handler interface for HTTPMockHandler.
func (m *HTTPMockHandler) Handle(_ context.Context, _ Request) Response {
return m.response
}
// InjectDecoder implements the DecoderInjector interface by setting the decoder.
func (m *HTTPMockHandler) InjectDecoder(decoder *Decoder) {
m.decoder = decoder
}
// mockBody simulates an error when reading the request body.
type mockBody struct{}
func (m *mockBody) Read(_ []byte) (n int, err error) {
return 0, errors.New("mock read error")
}
func (m *mockBody) Close() error {
return nil
}
// limitedBadResponseWriter is a custom io.Writer implementation that simulates
// write errors for a specified number of attempts. After a certain number of failures,
// it allows the write operation to succeed.
type limitedBadResponseWriter struct {
failCount int
maxFailures int
}
// Write simulates writing data to the writer. It forces an error response for
// a limited number of attempts, specified by maxFailures. Once failCount reaches
// maxFailures, it allows the write to succeed.
func (b *limitedBadResponseWriter) Write(p []byte) (n int, err error) {
if b.failCount < b.maxFailures {
b.failCount++
return 0, errors.New("forced write error")
}
// After reaching maxFailures, allow the write to succeed to stop the infinite loop.
return len(p), nil
}
func TestServeHTTP(t *testing.T) {
tests := []struct {
name string
req *http.Request
mockHandler *HTTPMockHandler
contentType string
res configv1alpha1.ResourceInterpreterContext
prep func(*http.Request, string) error
want *configv1alpha1.ResourceInterpreterResponse
}{
{
name: "ServeHTTP_EmptyBody_RequestFailed",
req: httptest.NewRequest(http.MethodPost, "/", nil),
mockHandler: &HTTPMockHandler{},
contentType: "application/json",
prep: func(req *http.Request, contentType string) error {
req.Header.Set("Content-Type", contentType)
req.Body = nil
return nil
},
want: &configv1alpha1.ResourceInterpreterResponse{
UID: "",
Successful: false,
Status: &configv1alpha1.RequestStatus{
Message: "request body is empty",
Code: http.StatusBadRequest,
},
},
},
{
name: "ServeHTTP_InvalidContentType_ContentTypeIsInvalid",
req: httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer([]byte(`{}`))),
mockHandler: &HTTPMockHandler{},
contentType: "text/plain",
prep: func(req *http.Request, contentType string) error {
req.Header.Set("Content-Type", contentType)
return nil
},
want: &configv1alpha1.ResourceInterpreterResponse{
UID: "",
Successful: false,
Status: &configv1alpha1.RequestStatus{
Message: "contentType=text/plain, expected application/json",
Code: http.StatusBadRequest,
},
},
},
{
name: "ServeHTTP_InvalidBodyJSON_JSONBodyIsInvalid",
req: httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer([]byte(`invalid-json`))),
mockHandler: &HTTPMockHandler{},
contentType: "application/json",
prep: func(req *http.Request, contentType string) error {
req.Header.Set("Content-Type", contentType)
return nil
},
want: &configv1alpha1.ResourceInterpreterResponse{
UID: "",
Successful: false,
Status: &configv1alpha1.RequestStatus{
Message: "json parse error",
Code: http.StatusBadRequest,
},
},
},
{
name: "ServeHTTP_ReadBodyError_FailedToReadBody",
req: httptest.NewRequest(http.MethodPost, "/", &mockBody{}),
mockHandler: &HTTPMockHandler{},
contentType: "application/json",
prep: func(req *http.Request, contentType string) error {
req.Header.Set("Content-Type", contentType)
return nil
},
want: &configv1alpha1.ResourceInterpreterResponse{
UID: "",
Successful: false,
Status: &configv1alpha1.RequestStatus{
Message: "mock read error",
Code: http.StatusBadRequest,
},
},
},
{
name: "ServeHTTP_ValidRequest_RequestIsValid",
req: httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer([]byte(`{}`))),
mockHandler: &HTTPMockHandler{
response: Response{
ResourceInterpreterResponse: configv1alpha1.ResourceInterpreterResponse{
Successful: true,
Status: &configv1alpha1.RequestStatus{Code: http.StatusOK},
},
},
},
contentType: "application/json",
prep: func(req *http.Request, contentType string) error {
req.Header.Set("Content-Type", contentType)
requestBody := configv1alpha1.ResourceInterpreterContext{
Request: &configv1alpha1.ResourceInterpreterRequest{
UID: "test-uid",
},
}
body, err := json.Marshal(requestBody)
if err != nil {
return fmt.Errorf("failed to marshal request body: %v", err)
}
req.Body = io.NopCloser(bytes.NewBuffer(body))
return nil
},
want: &configv1alpha1.ResourceInterpreterResponse{
UID: "test-uid",
Successful: true,
Status: &configv1alpha1.RequestStatus{
Message: "",
Code: http.StatusOK,
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
recorder := httptest.NewRecorder()
if err := test.prep(test.req, test.contentType); err != nil {
t.Errorf("failed to prep serving http: %v", err)
}
webhook := NewWebhook(test.mockHandler, &Decoder{})
webhook.ServeHTTP(recorder, test.req)
if err := verifyResourceInterpreterResponse(recorder.Body.Bytes(), test.want); err != nil {
t.Errorf("failed to verify resource interpreter response: %v", err)
}
})
}
}
func TestWriteResponse(t *testing.T) {
tests := []struct {
name string
res Response
rec *httptest.ResponseRecorder
mockHandler *HTTPMockHandler
decoder *Decoder
verify func([]byte, *configv1alpha1.ResourceInterpreterResponse) error
want *configv1alpha1.ResourceInterpreterResponse
}{
{
name: "WriteResponse_ValidValues_IsSucceeded",
res: Response{
ResourceInterpreterResponse: configv1alpha1.ResourceInterpreterResponse{
UID: "test-uid",
Successful: true,
Status: &configv1alpha1.RequestStatus{Code: http.StatusOK},
},
},
rec: httptest.NewRecorder(),
mockHandler: &HTTPMockHandler{},
decoder: &Decoder{},
verify: verifyResourceInterpreterResponse,
want: &configv1alpha1.ResourceInterpreterResponse{
UID: "test-uid",
Successful: true,
Status: &configv1alpha1.RequestStatus{
Message: "",
Code: http.StatusOK,
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
webhook := NewWebhook(test.mockHandler, test.decoder)
webhook.writeResponse(test.rec, test.res)
if err := test.verify(test.rec.Body.Bytes(), test.want); err != nil {
t.Errorf("failed to verify resource interpreter response: %v", err)
}
})
}
}
func TestWriteResourceInterpreterResponse(t *testing.T) {
tests := []struct {
name string
mockHandler *HTTPMockHandler
rec io.Writer
res configv1alpha1.ResourceInterpreterContext
verify func(io.Writer, *configv1alpha1.ResourceInterpreterResponse) error
want *configv1alpha1.ResourceInterpreterResponse
}{
{
name: "WriteResourceInterpreterResponse_ValidValues_WriteIsSuccessful",
mockHandler: &HTTPMockHandler{},
rec: httptest.NewRecorder(),
res: configv1alpha1.ResourceInterpreterContext{
Response: &configv1alpha1.ResourceInterpreterResponse{
UID: "test-uid",
Successful: true,
Status: &configv1alpha1.RequestStatus{Code: http.StatusOK},
},
},
verify: func(writer io.Writer, rir *configv1alpha1.ResourceInterpreterResponse) error {
data, ok := writer.(*httptest.ResponseRecorder)
if !ok {
return fmt.Errorf("expected writer of type httptest.ResponseRecorder but got %T", writer)
}
return verifyResourceInterpreterResponse(data.Body.Bytes(), rir)
},
want: &configv1alpha1.ResourceInterpreterResponse{
UID: "test-uid",
Successful: true,
Status: &configv1alpha1.RequestStatus{
Message: "",
Code: http.StatusOK,
},
},
},
{
name: "WriteResourceInterpreterResponse_FailedToWrite_WriterReachedMaxFailures",
mockHandler: &HTTPMockHandler{},
res: configv1alpha1.ResourceInterpreterContext{
Response: &configv1alpha1.ResourceInterpreterResponse{},
},
rec: &limitedBadResponseWriter{maxFailures: 3},
verify: func(writer io.Writer, _ *configv1alpha1.ResourceInterpreterResponse) error {
data, ok := writer.(*limitedBadResponseWriter)
if !ok {
return fmt.Errorf("expected writer of type limitedBadResponseWriter but got %T", writer)
}
if data.failCount != data.maxFailures {
return fmt.Errorf("expected %d write failures, got %d", data.maxFailures, data.failCount)
}
return nil
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
webhook := NewWebhook(test.mockHandler, &Decoder{})
webhook.writeResourceInterpreterResponse(test.rec, test.res)
if err := test.verify(test.rec, test.want); err != nil {
t.Errorf("failed to verify resource interpreter response: %v", err)
}
})
}
}
// verifyResourceInterpreterResponse unmarshals the provided body into a
// ResourceInterpreterContext and verifies it matches the expected values in res2.
func verifyResourceInterpreterResponse(body []byte, res2 *configv1alpha1.ResourceInterpreterResponse) error {
var resContext configv1alpha1.ResourceInterpreterContext
if err := json.Unmarshal(body, &resContext); err != nil {
return fmt.Errorf("failed to unmarshal body: %v", err)
}
if resContext.Response.UID != res2.UID {
return fmt.Errorf("expected UID %s, but got %s", res2.UID, resContext.Response.UID)
}
if resContext.Response.Successful != res2.Successful {
return fmt.Errorf("expected success status %t, but got %t", res2.Successful, resContext.Response.Successful)
}
if !strings.Contains(resContext.Response.Status.Message, res2.Status.Message) {
return fmt.Errorf("expected message %s to be subset, but got %s", res2.Status.Message, resContext.Response.Status.Message)
}
if resContext.Response.Status.Code != res2.Status.Code {
return fmt.Errorf("expected status code %d, but got %d", res2.Status.Code, resContext.Response.Status.Code)
}
return nil
}

View File

@ -0,0 +1,69 @@
/*
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 interpreter
import (
"testing"
)
// MockDecoderInjector is a mock struct implementing the DecoderInjector interface for testing purposes.
type MockDecoderInjector struct {
decoder *Decoder
}
// InjectDecoder implements the DecoderInjector interface by setting the decoder.
func (m *MockDecoderInjector) InjectDecoder(decoder *Decoder) {
m.decoder = decoder
}
func TestInjectDecoder(t *testing.T) {
tests := []struct {
name string
mockInjector interface{}
decoder *Decoder
wantToBeInjected bool
}{
{
name: "InjectDecoder_ObjectImplementsDecoderInjector_Injected",
mockInjector: &MockDecoderInjector{},
decoder: &Decoder{},
wantToBeInjected: true,
},
{
name: "InjectDecoder_ObjectNotImplementDecoderInjector_NotInjected",
mockInjector: struct{}{},
decoder: &Decoder{},
wantToBeInjected: false,
},
{
name: "InjectDecoder_ObjectImplementsDecoderInjector_Injected",
mockInjector: &MockDecoderInjector{},
wantToBeInjected: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if got := InjectDecoderInto(test.decoder, test.mockInjector); got != test.wantToBeInjected {
t.Errorf("expected status injection to be %t, but got %t", test.wantToBeInjected, got)
}
if test.wantToBeInjected && test.mockInjector.(*MockDecoderInjector).decoder != test.decoder {
t.Errorf("failed to inject the correct decoder, expected %v but got %v", test.decoder, test.mockInjector.(*MockDecoderInjector).decoder)
}
})
}
}

View File

@ -0,0 +1,179 @@
/*
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 interpreter
import (
"encoding/json"
"fmt"
"net/http"
"reflect"
"testing"
"gomodules.xyz/jsonpatch/v2"
configv1alpha1 "github.com/karmada-io/karmada/pkg/apis/config/v1alpha1"
)
// customError is a helper struct for simulating errors.
type customError struct {
msg string
}
func (e *customError) Error() string {
return e.msg
}
func TestErrored(t *testing.T) {
err := &customError{"Test Error"}
code := int32(500)
response := Errored(code, err)
expectedResponse := Response{
ResourceInterpreterResponse: configv1alpha1.ResourceInterpreterResponse{
Successful: false,
Status: &configv1alpha1.RequestStatus{
Code: code,
Message: err.Error(),
},
},
}
if !reflect.DeepEqual(expectedResponse, response) {
t.Errorf("response mismatch: expected %v, got %v", expectedResponse, response)
}
}
func TestSucceeded(t *testing.T) {
message := "Operation succeeded"
response := Succeeded(message)
expectedResponse := ValidationResponse(true, message)
if !reflect.DeepEqual(expectedResponse, response) {
t.Errorf("response mismatch: expected %v, got %v", expectedResponse, response)
}
}
func TestValidationResponse(t *testing.T) {
tests := []struct {
name string
msg string
isSuccessful bool
want Response
}{
{
name: "ValidationResponse_IsSuccessful_Succeeded",
msg: "Success",
isSuccessful: true,
want: Response{
ResourceInterpreterResponse: configv1alpha1.ResourceInterpreterResponse{
Successful: true,
Status: &configv1alpha1.RequestStatus{
Code: int32(http.StatusOK),
Message: "Success",
},
},
},
},
{
name: "ValidationResponse_IsFailed_Failed",
msg: "Failed",
isSuccessful: false,
want: Response{
ResourceInterpreterResponse: configv1alpha1.ResourceInterpreterResponse{
Successful: false,
Status: &configv1alpha1.RequestStatus{
Code: int32(http.StatusForbidden),
Message: "Failed",
},
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
response := ValidationResponse(test.isSuccessful, test.msg)
if !reflect.DeepEqual(response, test.want) {
t.Errorf("expected response %v but got response %v", test.want, response)
}
})
}
}
func TestPatchResponseFromRaw(t *testing.T) {
tests := []struct {
name string
original, current []byte
expectedPatch []jsonpatch.Operation
res Response
want Response
prep func(wantRes *Response) error
}{
{
name: "PatchResponseFromRaw_ReplacePatch_ReplacePatchExpected",
original: []byte(fmt.Sprintf(`{"name": "%s"}`, "original")),
current: []byte(fmt.Sprintf(`{"name": "%s"}`, "current")),
prep: func(wantRes *Response) error {
expectedPatch := []jsonpatch.Operation{
{
Operation: "replace",
Path: "/name",
Value: "current",
},
}
expectedPatchJSON, err := json.Marshal(expectedPatch)
if err != nil {
return fmt.Errorf("marshal failure: %v", err)
}
wantRes.ResourceInterpreterResponse.Patch = expectedPatchJSON
return nil
},
want: Response{
ResourceInterpreterResponse: configv1alpha1.ResourceInterpreterResponse{
Successful: true,
PatchType: func() *configv1alpha1.PatchType { pt := configv1alpha1.PatchTypeJSONPatch; return &pt }(),
},
},
},
{
name: "PatchResponseFromRaw_OriginalSameAsCurrentValue_NoPatchExpected",
original: []byte(fmt.Sprintf(`{"name": "%s"}`, "same")),
current: []byte(fmt.Sprintf(`{"name": "%s"}`, "same")),
prep: func(*Response) error { return nil },
want: Succeeded(""),
},
{
name: "PatchResponseFromRaw_InvalidJSONDocument_JSONDocumentIsInvalid",
original: []byte(nil),
current: []byte("updated"),
prep: func(*Response) error { return nil },
want: Errored(http.StatusInternalServerError, &customError{"invalid JSON Document"}),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if err := test.prep(&test.want); err != nil {
t.Errorf("failed to prep for patching response from raw: %v", err)
}
patchResponse := PatchResponseFromRaw(test.original, test.current)
if !reflect.DeepEqual(patchResponse, test.want) {
t.Errorf("unexpected error, patch responses not matched; expected %v, but got %v", patchResponse, test.want)
}
})
}
}

View File

@ -0,0 +1,158 @@
/*
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 interpreter
import (
"context"
"fmt"
"net/http"
"reflect"
"testing"
"k8s.io/apimachinery/pkg/types"
configv1alpha1 "github.com/karmada-io/karmada/pkg/apis/config/v1alpha1"
)
// WebhookMockHandler implements the Handler and DecoderInjector interfaces for testing.
type WebhookMockHandler struct {
response Response
decoder *Decoder
}
// Handle implements the Handler interface for WebhookMockHandler.
func (m *WebhookMockHandler) Handle(_ context.Context, _ Request) Response {
return m.response
}
// InjectDecoder implements the DecoderInjector interface by setting the decoder.
func (m *WebhookMockHandler) InjectDecoder(decoder *Decoder) {
m.decoder = decoder
}
func TestNewWebhook(t *testing.T) {
mockHandler := &WebhookMockHandler{}
decoder := &Decoder{}
webhook := NewWebhook(mockHandler, decoder)
if webhook == nil {
t.Fatalf("webhook returned by NewWebhook() is nil")
}
if webhook.handler != mockHandler {
t.Errorf("webhook has incorrect handler: expected %v, got %v", mockHandler, webhook.handler)
}
}
func TestHandle(t *testing.T) {
var uid types.UID = "test-uid"
expectedResponse := Response{
ResourceInterpreterResponse: configv1alpha1.ResourceInterpreterResponse{
Successful: true,
Status: &configv1alpha1.RequestStatus{
Code: http.StatusOK,
},
UID: uid,
},
}
mockHandler := &WebhookMockHandler{response: expectedResponse}
webhook := NewWebhook(mockHandler, &Decoder{})
req := Request{
ResourceInterpreterRequest: configv1alpha1.ResourceInterpreterRequest{
UID: uid,
},
}
resp := webhook.Handle(context.TODO(), req)
if !reflect.DeepEqual(resp, expectedResponse) {
t.Errorf("response mismatch in Handle(): expected %v, got %v", expectedResponse, resp)
}
if resp.UID != req.UID {
t.Errorf("uid was not set as expected: expected %v, got %v", req.UID, resp.UID)
}
}
func TestComplete(t *testing.T) {
tests := []struct {
name string
req Request
res Response
verify func(*Response, *Request) error
}{
{
name: "TestComplete_StatusAndStatusCodeAreUnset_FieldsArePopulated",
req: Request{
ResourceInterpreterRequest: configv1alpha1.ResourceInterpreterRequest{
UID: "test-uid",
},
},
res: Response{},
verify: verifyResourceInterpreterCompleteResponse,
},
{
name: "TestComplete_OverrideResponseUIDAndStatusCode_ResponseUIDAndStatusCodeAreOverrided",
req: Request{
ResourceInterpreterRequest: configv1alpha1.ResourceInterpreterRequest{
UID: "test-uid",
},
},
res: Response{
ResourceInterpreterResponse: configv1alpha1.ResourceInterpreterResponse{
UID: "existing-uid",
Status: &configv1alpha1.RequestStatus{
Code: http.StatusForbidden,
},
},
},
verify: func(resp *Response, req *Request) error {
if resp.UID != req.UID {
return fmt.Errorf("uid should be overridden if it's already set in the request: expected %v, got %v", req.UID, resp.UID)
}
if resp.Status.Code != http.StatusForbidden {
return fmt.Errorf("status code should not be overridden if it's already set: expected %v, got %v", http.StatusForbidden, resp.Status.Code)
}
return nil
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
test.res.Complete(test.req)
if err := test.verify(&test.res, &test.req); err != nil {
t.Errorf("failed to verify complete resource interpreter response: %v", err)
}
})
}
}
// verifyResourceInterpreterCompleteResponse checks if the response from
// the resource interpreter's Complete method is valid.
// It ensures the response UID matches the request UID, the Status is initialized,
// and the Status code is set to http.StatusOK. Returns an error if any check fails.
func verifyResourceInterpreterCompleteResponse(res *Response, req *Request) error {
if res.UID != req.UID {
return fmt.Errorf("uid was not set as expected: expected %v, got %v", req.UID, res.UID)
}
if res.Status == nil {
return fmt.Errorf("status should be initialized if it's nil")
}
if res.Status.Code != http.StatusOK {
return fmt.Errorf("status code should be set to %v if it was 0, got %v", http.StatusOK, res.Status.Code)
}
return nil
}