Merge pull request #5542 from mohamedawnallah/unitTestInterpreter
pkg/webhook: unit test Interpreter
This commit is contained in:
commit
376d20376c
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue