mirror of
https://github.com/IBM/fp-go.git
synced 2025-11-23 22:14:53 +02:00
394 lines
11 KiB
Go
394 lines
11 KiB
Go
// Copyright (c) 2023 - 2025 IBM Corp.
|
|
// All rights reserved.
|
|
//
|
|
// 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 http
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
H "net/http"
|
|
"net/url"
|
|
"testing"
|
|
|
|
E "github.com/IBM/fp-go/v2/either"
|
|
F "github.com/IBM/fp-go/v2/function"
|
|
C "github.com/IBM/fp-go/v2/http/content"
|
|
P "github.com/IBM/fp-go/v2/pair"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func NoError[A any](t *testing.T) func(E.Either[error, A]) bool {
|
|
return E.Fold(func(err error) bool {
|
|
return assert.NoError(t, err)
|
|
}, F.Constant1[A](true))
|
|
}
|
|
|
|
func Error[A any](t *testing.T) func(E.Either[error, A]) bool {
|
|
return E.Fold(F.Constant1[error](true), func(A) bool {
|
|
return assert.Error(t, nil)
|
|
})
|
|
}
|
|
|
|
func TestValidateJsonContentTypeString(t *testing.T) {
|
|
res := F.Pipe1(
|
|
validateJSONContentTypeString(C.JSON),
|
|
NoError[ParsedMediaType](t),
|
|
)
|
|
assert.True(t, res)
|
|
}
|
|
|
|
func TestValidateInvalidJsonContentTypeString(t *testing.T) {
|
|
res := F.Pipe1(
|
|
validateJSONContentTypeString("application/xml"),
|
|
Error[ParsedMediaType](t),
|
|
)
|
|
assert.True(t, res)
|
|
}
|
|
|
|
// TestParseMediaType tests parsing valid media types
|
|
func TestParseMediaType(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
mediaType string
|
|
wantType string
|
|
wantParam map[string]string
|
|
}{
|
|
{
|
|
name: "simple JSON",
|
|
mediaType: "application/json",
|
|
wantType: "application/json",
|
|
wantParam: map[string]string{},
|
|
},
|
|
{
|
|
name: "JSON with charset",
|
|
mediaType: "application/json; charset=utf-8",
|
|
wantType: "application/json",
|
|
wantParam: map[string]string{"charset": "utf-8"},
|
|
},
|
|
{
|
|
name: "HTML with charset",
|
|
mediaType: "text/html; charset=iso-8859-1",
|
|
wantType: "text/html",
|
|
wantParam: map[string]string{"charset": "iso-8859-1"},
|
|
},
|
|
{
|
|
name: "multipart with boundary",
|
|
mediaType: "multipart/form-data; boundary=----WebKitFormBoundary",
|
|
wantType: "multipart/form-data",
|
|
wantParam: map[string]string{"boundary": "----WebKitFormBoundary"},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := ParseMediaType(tt.mediaType)
|
|
require.True(t, E.IsRight(result), "ParseMediaType should succeed")
|
|
|
|
parsed := E.GetOrElse(func(error) ParsedMediaType {
|
|
return P.MakePair("", map[string]string{})
|
|
})(result)
|
|
mediaType := P.Head(parsed)
|
|
params := P.Tail(parsed)
|
|
|
|
assert.Equal(t, tt.wantType, mediaType)
|
|
assert.Equal(t, tt.wantParam, params)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestParseMediaTypeInvalid tests parsing invalid media types
|
|
func TestParseMediaTypeInvalid(t *testing.T) {
|
|
result := ParseMediaType("invalid media type")
|
|
assert.True(t, E.IsLeft(result), "ParseMediaType should fail for invalid input")
|
|
}
|
|
|
|
// TestHttpErrorMethods tests all HttpError methods
|
|
func TestHttpErrorMethods(t *testing.T) {
|
|
testURL, _ := url.Parse("https://example.com/api/test")
|
|
headers := make(H.Header)
|
|
headers.Set("Content-Type", "application/json")
|
|
headers.Set("X-Custom", "value")
|
|
body := []byte(`{"error": "not found"}`)
|
|
|
|
httpErr := &HttpError{
|
|
statusCode: 404,
|
|
headers: headers,
|
|
body: body,
|
|
url: testURL,
|
|
}
|
|
|
|
// Test StatusCode
|
|
assert.Equal(t, 404, httpErr.StatusCode())
|
|
|
|
// Test Headers
|
|
returnedHeaders := httpErr.Headers()
|
|
assert.Equal(t, "application/json", returnedHeaders.Get("Content-Type"))
|
|
assert.Equal(t, "value", returnedHeaders.Get("X-Custom"))
|
|
|
|
// Test Body
|
|
assert.Equal(t, body, httpErr.Body())
|
|
assert.Equal(t, `{"error": "not found"}`, string(httpErr.Body()))
|
|
|
|
// Test URL
|
|
assert.Equal(t, testURL, httpErr.URL())
|
|
assert.Equal(t, "https://example.com/api/test", httpErr.URL().String())
|
|
|
|
// Test Error
|
|
errMsg := httpErr.Error()
|
|
assert.Contains(t, errMsg, "404")
|
|
assert.Contains(t, errMsg, "https://example.com/api/test")
|
|
|
|
// Test String
|
|
assert.Equal(t, errMsg, httpErr.String())
|
|
}
|
|
|
|
// TestGetHeader tests the GetHeader function
|
|
func TestGetHeader(t *testing.T) {
|
|
resp := &H.Response{
|
|
Header: make(H.Header),
|
|
}
|
|
resp.Header.Set("Content-Type", "application/json")
|
|
resp.Header.Set("Authorization", "Bearer token")
|
|
|
|
headers := GetHeader(resp)
|
|
assert.Equal(t, "application/json", headers.Get("Content-Type"))
|
|
assert.Equal(t, "Bearer token", headers.Get("Authorization"))
|
|
}
|
|
|
|
// TestGetBody tests the GetBody function
|
|
func TestGetBody(t *testing.T) {
|
|
bodyContent := []byte("test body content")
|
|
resp := &H.Response{
|
|
Body: io.NopCloser(bytes.NewReader(bodyContent)),
|
|
}
|
|
|
|
body := GetBody(resp)
|
|
defer body.Close()
|
|
|
|
data, err := io.ReadAll(body)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, bodyContent, data)
|
|
}
|
|
|
|
// TestIsValidStatus tests the isValidStatus function
|
|
func TestIsValidStatus(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
statusCode int
|
|
want bool
|
|
}{
|
|
{"200 OK", H.StatusOK, true},
|
|
{"201 Created", H.StatusCreated, true},
|
|
{"204 No Content", H.StatusNoContent, true},
|
|
{"299 (edge of 2xx)", 299, true},
|
|
{"300 Multiple Choices", H.StatusMultipleChoices, false},
|
|
{"301 Moved Permanently", H.StatusMovedPermanently, false},
|
|
{"400 Bad Request", H.StatusBadRequest, false},
|
|
{"404 Not Found", H.StatusNotFound, false},
|
|
{"500 Internal Server Error", H.StatusInternalServerError, false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
resp := &H.Response{StatusCode: tt.statusCode}
|
|
assert.Equal(t, tt.want, isValidStatus(resp))
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestValidateResponse tests the ValidateResponse function
|
|
func TestValidateResponse(t *testing.T) {
|
|
t.Run("successful response", func(t *testing.T) {
|
|
resp := &H.Response{
|
|
StatusCode: H.StatusOK,
|
|
Header: make(H.Header),
|
|
}
|
|
|
|
result := ValidateResponse(resp)
|
|
assert.True(t, E.IsRight(result))
|
|
|
|
validResp := E.GetOrElse(func(error) *H.Response { return nil })(result)
|
|
assert.Equal(t, resp, validResp)
|
|
})
|
|
|
|
t.Run("error response", func(t *testing.T) {
|
|
testURL, _ := url.Parse("https://example.com/test")
|
|
resp := &H.Response{
|
|
StatusCode: H.StatusNotFound,
|
|
Header: make(H.Header),
|
|
Body: io.NopCloser(bytes.NewReader([]byte("not found"))),
|
|
Request: &H.Request{URL: testURL},
|
|
}
|
|
|
|
result := ValidateResponse(resp)
|
|
assert.True(t, E.IsLeft(result))
|
|
|
|
// Extract error using Fold
|
|
var httpErr *HttpError
|
|
E.Fold(
|
|
func(err error) *H.Response {
|
|
var ok bool
|
|
httpErr, ok = err.(*HttpError)
|
|
require.True(t, ok, "error should be *HttpError")
|
|
return nil
|
|
},
|
|
func(r *H.Response) *H.Response { return r },
|
|
)(result)
|
|
assert.Equal(t, 404, httpErr.StatusCode())
|
|
})
|
|
}
|
|
|
|
// TestStatusCodeError tests the StatusCodeError function
|
|
func TestStatusCodeError(t *testing.T) {
|
|
testURL, _ := url.Parse("https://api.example.com/users/123")
|
|
bodyContent := []byte(`{"error": "user not found"}`)
|
|
|
|
headers := make(H.Header)
|
|
headers.Set("Content-Type", "application/json")
|
|
headers.Set("X-Request-ID", "abc123")
|
|
|
|
resp := &H.Response{
|
|
StatusCode: H.StatusNotFound,
|
|
Header: headers,
|
|
Body: io.NopCloser(bytes.NewReader(bodyContent)),
|
|
Request: &H.Request{URL: testURL},
|
|
}
|
|
|
|
err := StatusCodeError(resp)
|
|
require.Error(t, err)
|
|
|
|
httpErr, ok := err.(*HttpError)
|
|
require.True(t, ok, "error should be *HttpError")
|
|
|
|
// Verify all fields
|
|
assert.Equal(t, 404, httpErr.StatusCode())
|
|
assert.Equal(t, testURL, httpErr.URL())
|
|
assert.Equal(t, bodyContent, httpErr.Body())
|
|
|
|
// Verify headers are cloned
|
|
returnedHeaders := httpErr.Headers()
|
|
assert.Equal(t, "application/json", returnedHeaders.Get("Content-Type"))
|
|
assert.Equal(t, "abc123", returnedHeaders.Get("X-Request-ID"))
|
|
|
|
// Verify error message
|
|
errMsg := httpErr.Error()
|
|
assert.Contains(t, errMsg, "404")
|
|
assert.Contains(t, errMsg, "https://api.example.com/users/123")
|
|
}
|
|
|
|
// TestValidateJSONResponse tests the ValidateJSONResponse function
|
|
func TestValidateJSONResponse(t *testing.T) {
|
|
t.Run("valid JSON response", func(t *testing.T) {
|
|
resp := &H.Response{
|
|
StatusCode: H.StatusOK,
|
|
Header: make(H.Header),
|
|
}
|
|
resp.Header.Set("Content-Type", "application/json")
|
|
|
|
result := ValidateJSONResponse(resp)
|
|
assert.True(t, E.IsRight(result), "should accept valid JSON response")
|
|
})
|
|
|
|
t.Run("JSON with charset", func(t *testing.T) {
|
|
resp := &H.Response{
|
|
StatusCode: H.StatusOK,
|
|
Header: make(H.Header),
|
|
}
|
|
resp.Header.Set("Content-Type", "application/json; charset=utf-8")
|
|
|
|
result := ValidateJSONResponse(resp)
|
|
assert.True(t, E.IsRight(result), "should accept JSON with charset")
|
|
})
|
|
|
|
t.Run("JSON variant (hal+json)", func(t *testing.T) {
|
|
resp := &H.Response{
|
|
StatusCode: H.StatusOK,
|
|
Header: make(H.Header),
|
|
}
|
|
resp.Header.Set("Content-Type", "application/hal+json")
|
|
|
|
result := ValidateJSONResponse(resp)
|
|
assert.True(t, E.IsRight(result), "should accept JSON variants")
|
|
})
|
|
|
|
t.Run("non-JSON content type", func(t *testing.T) {
|
|
resp := &H.Response{
|
|
StatusCode: H.StatusOK,
|
|
Header: make(H.Header),
|
|
}
|
|
resp.Header.Set("Content-Type", "text/html")
|
|
|
|
result := ValidateJSONResponse(resp)
|
|
assert.True(t, E.IsLeft(result), "should reject non-JSON content type")
|
|
})
|
|
|
|
t.Run("missing Content-Type header", func(t *testing.T) {
|
|
resp := &H.Response{
|
|
StatusCode: H.StatusOK,
|
|
Header: make(H.Header),
|
|
}
|
|
|
|
result := ValidateJSONResponse(resp)
|
|
assert.True(t, E.IsLeft(result), "should reject missing Content-Type")
|
|
})
|
|
|
|
t.Run("valid JSON with error status code", func(t *testing.T) {
|
|
// Note: ValidateJSONResponse only validates Content-Type, not status code
|
|
// It wraps the response in Right(response) first, then validates headers
|
|
resp := &H.Response{
|
|
StatusCode: H.StatusInternalServerError,
|
|
Header: make(H.Header),
|
|
}
|
|
resp.Header.Set("Content-Type", "application/json")
|
|
|
|
result := ValidateJSONResponse(resp)
|
|
// This actually succeeds because ValidateJSONResponse doesn't check status
|
|
assert.True(t, E.IsRight(result), "ValidateJSONResponse only checks Content-Type, not status")
|
|
})
|
|
}
|
|
|
|
// TestFullResponseAccessors tests Response and Body accessors
|
|
func TestFullResponseAccessors(t *testing.T) {
|
|
resp := &H.Response{
|
|
StatusCode: H.StatusOK,
|
|
Header: make(H.Header),
|
|
}
|
|
resp.Header.Set("Content-Type", "application/json")
|
|
|
|
bodyContent := []byte(`{"message": "success"}`)
|
|
fullResp := P.MakePair(resp, bodyContent)
|
|
|
|
// Test Response accessor
|
|
extractedResp := Response(fullResp)
|
|
assert.Equal(t, resp, extractedResp)
|
|
assert.Equal(t, H.StatusOK, extractedResp.StatusCode)
|
|
|
|
// Test Body accessor
|
|
extractedBody := Body(fullResp)
|
|
assert.Equal(t, bodyContent, extractedBody)
|
|
assert.Equal(t, `{"message": "success"}`, string(extractedBody))
|
|
}
|
|
|
|
// TestHeaderContentTypeConstant tests the HeaderContentType constant
|
|
func TestHeaderContentTypeConstant(t *testing.T) {
|
|
assert.Equal(t, "Content-Type", HeaderContentType)
|
|
|
|
// Test usage with http.Header
|
|
headers := make(H.Header)
|
|
headers.Set(HeaderContentType, "application/json")
|
|
assert.Equal(t, "application/json", headers.Get(HeaderContentType))
|
|
}
|