Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 0 additions & 11 deletions makefiles/test_requests.mk
Original file line number Diff line number Diff line change
Expand Up @@ -52,17 +52,6 @@ check_path_up_envoy:
exit 1; \
fi

.PHONY: check_relay_util
# Internal helper: Checks if relay-util is installed locally
check_relay_util:
@if ! command -v relay-util &> /dev/null; then \
echo "####################################################################################################"; \
echo "Relay Util is not installed." \
echo "To use any Relay Util make targets to send load testing requests please install Relay Util with:"; \
echo "go install github.com/commoddity/relay-util/v2@latest"; \
echo "####################################################################################################"; \
fi

###################################
#### PATH binary Test Requests ####
###################################
Expand Down
179 changes: 179 additions & 0 deletions network/http/http_client_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
package http

import (
"bytes"
"fmt"
"io"
"net/http"
"testing"

"github.com/stretchr/testify/require"

"github.com/buildwithgrove/path/network/concurrency"
)

func TestEnsureHTTPSuccess(t *testing.T) {
Expand Down Expand Up @@ -114,3 +119,177 @@ func TestEnsureHTTPSuccess(t *testing.T) {
})
}
}

// TestReadAndValidateResponse_Integration tests the complete HTTP response validation flow
func TestReadAndValidateResponse_Integration(t *testing.T) {
// Create a minimal HTTP client for testing
client := &HTTPClientWithDebugMetrics{
bufferPool: concurrency.NewBufferPool(1024), // 1KB max buffer size
}

tests := []struct {
name string
statusCode int
body string
expectError bool
expectedErrMsg string
}{
{
name: "successful response with JSON",
statusCode: http.StatusOK,
body: `{"result":"success","data":{"value":42}}`,
expectError: false,
},
{
name: "successful response with empty body",
statusCode: http.StatusNoContent,
body: "",
expectError: false,
},
{
name: "bad request error",
statusCode: http.StatusBadRequest,
body: `{"error":"invalid request"}`,
expectError: true,
expectedErrMsg: "400",
},
{
name: "internal server error",
statusCode: http.StatusInternalServerError,
body: `{"error":"server error"}`,
expectError: true,
expectedErrMsg: "500",
},
{
name: "bad gateway error",
statusCode: http.StatusBadGateway,
body: "<html><body>502 Bad Gateway</body></html>",
expectError: true,
expectedErrMsg: "502",
},
{
name: "service unavailable",
statusCode: http.StatusServiceUnavailable,
body: "Service temporarily unavailable",
expectError: true,
expectedErrMsg: "503",
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Create mock HTTP response
resp := &http.Response{
StatusCode: tc.statusCode,
Body: io.NopCloser(bytes.NewBufferString(tc.body)),
}

// Test the validation function
responseBody, err := client.readAndValidateResponse(resp)

if tc.expectError {
require.Error(t, err)
require.ErrorIs(t, err, ErrRelayEndpointHTTPError)
require.Contains(t, err.Error(), tc.expectedErrMsg)
require.Nil(t, responseBody)
} else {
require.NoError(t, err)
require.NotNil(t, responseBody)
require.Equal(t, tc.body, string(responseBody))
}
})
}
}

// TestReadAndValidateResponse_ErrorCases tests edge cases and error conditions
func TestReadAndValidateResponse_ErrorCases(t *testing.T) {
client := &HTTPClientWithDebugMetrics{
bufferPool: concurrency.NewBufferPool(10), // Very small buffer for testing limits
}

tests := []struct {
name string
setupResponse func() *http.Response
expectError bool
expectedErrMsg string
}{
{
name: "large response body exceeding buffer",
setupResponse: func() *http.Response {
largeBody := bytes.Repeat([]byte("x"), 2048) // 2KB body, 10B buffer
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewBuffer(largeBody)),
}
},
expectError: true,
expectedErrMsg: "failed to read response body",
},
{
name: "body read error",
setupResponse: func() *http.Response {
return &http.Response{
StatusCode: http.StatusOK,
Body: &errorReader{},
}
},
expectError: true,
expectedErrMsg: "failed to read response body",
},
{
name: "successful large response within buffer limits",
setupResponse: func() *http.Response {
smallBody := []byte("small response")
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewBuffer(smallBody)),
}
},
expectError: false,
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
resp := tc.setupResponse()
responseBody, err := client.readAndValidateResponse(resp)

if tc.expectError {
require.Error(t, err)
require.Contains(t, err.Error(), tc.expectedErrMsg)
require.Nil(t, responseBody)
} else {
require.NoError(t, err)
require.NotNil(t, responseBody)
}
})
}
}

// TestEnsureHTTPSuccess_ErrorWrapping tests error wrapping behavior
func TestEnsureHTTPSuccess_ErrorWrapping(t *testing.T) {
testCodes := []int{400, 500, 502, 503}

for _, code := range testCodes {
t.Run(fmt.Sprintf("status_%d", code), func(t *testing.T) {
err := EnsureHTTPSuccess(code)

require.Error(t, err)
require.ErrorIs(t, err, ErrRelayEndpointHTTPError)

// Verify the error message contains the status code
require.Contains(t, err.Error(), fmt.Sprintf("%d", code))
})
}
}

// errorReader is a mock io.ReadCloser that always returns an error
type errorReader struct{}

func (e *errorReader) Read([]byte) (int, error) {
return 0, fmt.Errorf("mock read error")
}

func (e *errorReader) Close() error {
return nil
}
79 changes: 79 additions & 0 deletions protocol/shannon/context_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package shannon

import (
"testing"

"github.com/stretchr/testify/require"

pathhttp "github.com/buildwithgrove/path/network/http"
)

// TestBackendServiceHTTPValidation tests the backend service HTTP status validation logic
// This tests the EnsureHTTPSuccess function integration in the Shannon protocol
func TestBackendServiceHTTPValidation(t *testing.T) {

tests := []struct {
name string
statusCode int
expectError bool
expectedErrMsg string
}{
{"successful_200_response", 200, false, ""},
{"successful_201_response", 201, false, ""},
{"successful_204_no_content", 204, false, ""},
{"bad_request_400", 400, true, "400"},
{"unauthorized_401", 401, true, "401"},
{"not_found_404", 404, true, "404"},
{"internal_server_error_500", 500, true, "500"},
{"bad_gateway_502", 502, true, "502"},
{"service_unavailable_503", 503, true, "503"},
{"gateway_timeout_504", 504, true, "504"},
{"edge_case_1xx_continue", 100, true, "100"},
{"edge_case_3xx_redirect", 301, true, "301"},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Test the HTTP validation logic directly used by Shannon protocol
err := pathhttp.EnsureHTTPSuccess(tc.statusCode)

if tc.expectError {
require.Error(t, err)
require.ErrorIs(t, err, pathhttp.ErrRelayEndpointHTTPError)
require.Contains(t, err.Error(), tc.expectedErrMsg)
} else {
require.NoError(t, err)
}
})
}
}

// TestHTTPValidationBoundaryValues tests edge cases for HTTP status validation
func TestHTTPValidationBoundaryValues(t *testing.T) {
boundaryTests := []struct {
name string
statusCode int
expectError bool
}{
{"lowest_2xx_boundary", 200, false},
{"highest_2xx_boundary", 299, false},
{"just_below_2xx", 199, true},
{"just_above_2xx", 300, true},
{"zero_status", 0, true},
{"negative_status", -1, true},
{"very_high_status", 999, true},
}

for _, tc := range boundaryTests {
t.Run(tc.name, func(t *testing.T) {
err := pathhttp.EnsureHTTPSuccess(tc.statusCode)

if tc.expectError {
require.Error(t, err)
require.ErrorIs(t, err, pathhttp.ErrRelayEndpointHTTPError)
} else {
require.NoError(t, err)
}
})
}
}
Loading
Loading