347 lines
11 KiB
Go
347 lines
11 KiB
Go
/*
|
|
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
|
|
}
|