1
0
Fork 0
golang-github-meilisearch-m.../client_test.go
Daniel Baumann 5d4914ed7f
Adding upstream version 0.31.1.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-05-18 21:42:39 +02:00

504 lines
14 KiB
Go

package meilisearch
import (
"bytes"
"context"
"encoding/json"
"errors"
"io"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/require"
)
// Mock structures for testing
type mockResponse struct {
Message string `json:"message"`
}
type mockJsonMarshaller struct {
valid bool
null bool
Foo string `json:"foo"`
Bar string `json:"bar"`
}
// failingEncoder is used to simulate encoder failure
type failingEncoder struct{}
func (fe failingEncoder) Encode(r io.Reader) (*bytes.Buffer, error) {
return nil, errors.New("dummy encoding failure")
}
// Implement Decode method to satisfy the encoder interface, though it won't be used here
func (fe failingEncoder) Decode(b []byte, v interface{}) error {
return errors.New("dummy decode failure")
}
func TestExecuteRequest(t *testing.T) {
retryCount := 0
// Create a mock server
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet && r.URL.Path == "/test-get" {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"message":"get successful"}`))
} else if r.Method == http.MethodGet && r.URL.Path == "/test-get-encoding" {
encode := r.Header.Get("Accept-Encoding")
if len(encode) != 0 {
enc := newEncoding(ContentEncoding(encode), DefaultCompression)
d := &mockData{Name: "foo", Age: 30}
b, err := json.Marshal(d)
require.NoError(t, err)
res, err := enc.Encode(bytes.NewReader(b))
require.NoError(t, err)
_, _ = w.Write(res.Bytes())
w.WriteHeader(http.StatusOK)
return
}
_, _ = w.Write([]byte("invalid message"))
w.WriteHeader(http.StatusInternalServerError)
} else if r.Method == http.MethodPost && r.URL.Path == "/test-req-resp-encoding" {
accept := r.Header.Get("Accept-Encoding")
ce := r.Header.Get("Content-Encoding")
reqEnc := newEncoding(ContentEncoding(ce), DefaultCompression)
respEnc := newEncoding(ContentEncoding(accept), DefaultCompression)
req := new(mockData)
if len(ce) != 0 {
b, err := io.ReadAll(r.Body)
require.NoError(t, err)
err = reqEnc.Decode(b, req)
require.NoError(t, err)
}
if len(accept) != 0 {
d, err := json.Marshal(req)
require.NoError(t, err)
res, err := respEnc.Encode(bytes.NewReader(d))
require.NoError(t, err)
_, _ = w.Write(res.Bytes())
w.WriteHeader(http.StatusOK)
}
} else if r.Method == http.MethodPost && r.URL.Path == "/test-post" {
w.WriteHeader(http.StatusCreated)
msg := []byte(`{"message":"post successful"}`)
_, _ = w.Write(msg)
} else if r.Method == http.MethodGet && r.URL.Path == "/test-null-body" {
w.WriteHeader(http.StatusOK)
msg := []byte(`null`)
_, _ = w.Write(msg)
} else if r.Method == http.MethodPost && r.URL.Path == "/test-post-encoding" {
w.WriteHeader(http.StatusCreated)
msg := []byte(`{"message":"post successful"}`)
enc := r.Header.Get("Accept-Encoding")
if len(enc) != 0 {
e := newEncoding(ContentEncoding(enc), DefaultCompression)
b, err := e.Encode(bytes.NewReader(msg))
require.NoError(t, err)
_, _ = w.Write(b.Bytes())
return
}
_, _ = w.Write(msg)
} else if r.URL.Path == "/test-bad-request" {
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte(`{"message":"bad request"}`))
} else if r.URL.Path == "/invalid-response-body" {
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(`{"message":"bad response body"}`))
} else if r.URL.Path == "/io-reader" {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"message":"io reader"}`))
} else if r.URL.Path == "/failed-retry" {
w.WriteHeader(http.StatusBadGateway)
} else if r.URL.Path == "/success-retry" {
if retryCount == 2 {
w.WriteHeader(http.StatusOK)
return
}
w.WriteHeader(http.StatusBadGateway)
retryCount++
} else if r.URL.Path == "/dummy" {
w.WriteHeader(http.StatusOK)
} else {
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
tests := []struct {
name string
internalReq *internalRequest
expectedResp interface{}
contentEncoding ContentEncoding
withTimeout bool
disableRetry bool
wantErr bool
}{
{
name: "Successful GET request",
internalReq: &internalRequest{
endpoint: "/test-get",
method: http.MethodGet,
withResponse: &mockResponse{},
acceptedStatusCodes: []int{http.StatusOK},
},
expectedResp: &mockResponse{Message: "get successful"},
wantErr: false,
},
{
name: "Successful POST request",
internalReq: &internalRequest{
endpoint: "/test-post",
method: http.MethodPost,
withRequest: map[string]string{"key": "value"},
contentType: contentTypeJSON,
withResponse: &mockResponse{},
acceptedStatusCodes: []int{http.StatusCreated},
},
expectedResp: &mockResponse{Message: "post successful"},
wantErr: false,
},
{
name: "404 Not Found",
internalReq: &internalRequest{
endpoint: "/not-found",
method: http.MethodGet,
withResponse: &mockResponse{},
acceptedStatusCodes: []int{http.StatusOK},
},
expectedResp: nil,
wantErr: true,
},
{
name: "Invalid URL",
internalReq: &internalRequest{
endpoint: "/invalid-url$%^*()*#",
method: http.MethodGet,
withResponse: &mockResponse{},
acceptedStatusCodes: []int{http.StatusOK},
},
expectedResp: nil,
wantErr: true,
},
{
name: "Invalid response body",
internalReq: &internalRequest{
endpoint: "/invalid-response-body",
method: http.MethodGet,
withResponse: struct{}{},
acceptedStatusCodes: []int{http.StatusInternalServerError},
},
expectedResp: nil,
wantErr: true,
},
{
name: "Invalid request method",
internalReq: &internalRequest{
endpoint: "/invalid-request-method",
method: http.MethodGet,
withResponse: nil,
withRequest: struct{}{},
acceptedStatusCodes: []int{http.StatusBadRequest},
},
expectedResp: nil,
wantErr: true,
},
{
name: "Invalid request content type",
internalReq: &internalRequest{
endpoint: "/invalid-request-content-type",
method: http.MethodPost,
withResponse: nil,
contentType: "",
withRequest: struct{}{},
acceptedStatusCodes: []int{http.StatusBadRequest},
},
expectedResp: nil,
wantErr: true,
},
{
name: "Invalid json marshaler",
internalReq: &internalRequest{
endpoint: "/invalid-marshaler",
method: http.MethodPost,
withResponse: nil,
withRequest: &mockJsonMarshaller{
valid: false,
},
contentType: "application/json",
},
expectedResp: nil,
wantErr: true,
},
{
name: "Null data marshaler",
internalReq: &internalRequest{
endpoint: "/null-data-marshaler",
method: http.MethodPost,
withResponse: nil,
withRequest: &mockJsonMarshaller{
valid: true,
null: true,
},
contentType: "application/json",
},
expectedResp: nil,
wantErr: true,
},
{
name: "Test null body response",
internalReq: &internalRequest{
endpoint: "/test-null-body",
method: http.MethodGet,
withResponse: make([]byte, 0),
contentType: "application/json",
acceptedStatusCodes: []int{http.StatusOK},
},
expectedResp: nil,
wantErr: false,
},
{
name: "400 Bad Request",
internalReq: &internalRequest{
endpoint: "/test-bad-request",
method: http.MethodGet,
withResponse: &mockResponse{},
acceptedStatusCodes: []int{http.StatusOK},
},
expectedResp: nil,
wantErr: true,
},
{
name: "Test request encoding gzip",
internalReq: &internalRequest{
endpoint: "/test-post-encoding",
method: http.MethodPost,
withRequest: map[string]string{"key": "value"},
contentType: contentTypeJSON,
withResponse: &mockResponse{},
acceptedStatusCodes: []int{http.StatusCreated},
},
expectedResp: &mockResponse{Message: "post successful"},
contentEncoding: GzipEncoding,
wantErr: false,
},
{
name: "Test request encoding deflate",
internalReq: &internalRequest{
endpoint: "/test-post-encoding",
method: http.MethodPost,
withRequest: map[string]string{"key": "value"},
contentType: contentTypeJSON,
withResponse: &mockResponse{},
acceptedStatusCodes: []int{http.StatusCreated},
},
expectedResp: &mockResponse{Message: "post successful"},
contentEncoding: DeflateEncoding,
wantErr: false,
},
{
name: "Test request encoding brotli",
internalReq: &internalRequest{
endpoint: "/test-post-encoding",
method: http.MethodPost,
withRequest: map[string]string{"key": "value"},
contentType: contentTypeJSON,
withResponse: &mockResponse{},
acceptedStatusCodes: []int{http.StatusCreated},
},
expectedResp: &mockResponse{Message: "post successful"},
contentEncoding: BrotliEncoding,
wantErr: false,
},
{
name: "Test response decoding gzip",
internalReq: &internalRequest{
endpoint: "/test-get-encoding",
method: http.MethodGet,
withRequest: nil,
withResponse: &mockData{},
acceptedStatusCodes: []int{http.StatusOK},
},
expectedResp: &mockData{Name: "foo", Age: 30},
contentEncoding: GzipEncoding,
wantErr: false,
},
{
name: "Test response decoding deflate",
internalReq: &internalRequest{
endpoint: "/test-get-encoding",
method: http.MethodGet,
withRequest: nil,
withResponse: &mockData{},
acceptedStatusCodes: []int{http.StatusOK},
},
expectedResp: &mockData{Name: "foo", Age: 30},
contentEncoding: DeflateEncoding,
wantErr: false,
},
{
name: "Test response decoding brotli",
internalReq: &internalRequest{
endpoint: "/test-get-encoding",
method: http.MethodGet,
withRequest: nil,
withResponse: &mockData{},
acceptedStatusCodes: []int{http.StatusOK},
},
expectedResp: &mockData{Name: "foo", Age: 30},
contentEncoding: BrotliEncoding,
wantErr: false,
},
{
name: "Test request and response encoding",
internalReq: &internalRequest{
endpoint: "/test-req-resp-encoding",
method: http.MethodPost,
contentType: contentTypeJSON,
withRequest: &mockData{Name: "foo", Age: 30},
withResponse: &mockData{},
acceptedStatusCodes: []int{http.StatusOK},
},
expectedResp: &mockData{Name: "foo", Age: 30},
contentEncoding: GzipEncoding,
wantErr: false,
},
{
name: "Test successful retries",
internalReq: &internalRequest{
endpoint: "/success-retry",
method: http.MethodGet,
withResponse: nil,
withRequest: nil,
acceptedStatusCodes: []int{http.StatusOK},
},
expectedResp: nil,
wantErr: false,
},
{
name: "Test failed retries",
internalReq: &internalRequest{
endpoint: "/failed-retry",
method: http.MethodGet,
withResponse: nil,
withRequest: nil,
acceptedStatusCodes: []int{http.StatusOK},
},
expectedResp: nil,
wantErr: true,
},
{
name: "Test disable retries",
internalReq: &internalRequest{
endpoint: "/test-get",
method: http.MethodGet,
withResponse: nil,
withRequest: nil,
acceptedStatusCodes: []int{http.StatusOK},
},
expectedResp: nil,
disableRetry: true,
wantErr: false,
},
{
name: "Test request timeout on retries",
internalReq: &internalRequest{
endpoint: "/failed-retry",
method: http.MethodGet,
withResponse: nil,
withRequest: nil,
acceptedStatusCodes: []int{http.StatusOK},
},
expectedResp: nil,
withTimeout: true,
wantErr: true,
},
{
name: "Test request encoding with []byte encode failure",
internalReq: &internalRequest{
endpoint: "/dummy",
method: http.MethodPost,
withRequest: []byte("test data"),
contentType: "text/plain",
acceptedStatusCodes: []int{http.StatusOK},
},
expectedResp: nil,
contentEncoding: GzipEncoding,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := newClient(&http.Client{}, ts.URL, "testApiKey", clientConfig{
contentEncoding: tt.contentEncoding,
encodingCompressionLevel: DefaultCompression,
maxRetries: 3,
disableRetry: tt.disableRetry,
retryOnStatus: map[int]bool{
502: true,
503: true,
504: true,
},
})
// For the specific test case, override the encoder to force an error
if tt.name == "Test request encoding with []byte encode failure" {
c.encoder = failingEncoder{}
}
ctx := context.Background()
if tt.withTimeout {
timeoutCtx, cancel := context.WithTimeout(ctx, 1*time.Second)
ctx = timeoutCtx
defer cancel()
}
err := c.executeRequest(ctx, tt.internalReq)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Equal(t, tt.expectedResp, tt.internalReq.withResponse)
}
})
}
}
func TestNewClientNilRetryOnStatus(t *testing.T) {
c := newClient(&http.Client{}, "", "", clientConfig{
maxRetries: 3,
retryOnStatus: nil,
})
require.NotNil(t, c.retryOnStatus)
}
func (m mockJsonMarshaller) MarshalJSON() ([]byte, error) {
type Alias mockJsonMarshaller
if !m.valid {
return nil, errors.New("mockJsonMarshaller not valid")
}
if m.null {
return nil, nil
}
return json.Marshal(&struct {
Alias
}{
Alias: Alias(m),
})
}