blob: 102cdafc8adfdb653d91c6c2062e44b513ccd07a [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"testing"
)
// mockResponseWriter is a test implementation of http.ResponseWriter
type mockResponseWriter struct {
header http.Header
}
func (w *mockResponseWriter) Header() http.Header {
if w.header == nil {
w.header = make(http.Header)
}
return w.header
}
func (w *mockResponseWriter) Write(b []byte) (int, error) {
return len(b), nil
}
func (w *mockResponseWriter) WriteHeader(statusCode int) {
}
// newTestHTTPContext creates a mock HttpContext for testing
func newTestHTTPContext(r *http.Request) *HttpContext {
ctx := &HttpContext{
Index: -1,
Request: r,
Writer: &mockResponseWriter{},
Ctx: context.Background(),
}
ctx.Reset()
return ctx
}
// TestErrorBuilder tests the ErrorBuilder methods
func TestErrorBuilder(t *testing.T) {
tests := []struct {
name string
builder *ErrorBuilder
expectedStatus int
expectedMsg string
}{
{
name: "BadRequest",
builder: BadRequest,
expectedStatus: http.StatusBadRequest,
expectedMsg: "Bad request",
},
{
name: "NotFound",
builder: RouteNotFound,
expectedStatus: http.StatusNotFound,
expectedMsg: "Route not found",
},
{
name: "InternalError",
builder: InternalError,
expectedStatus: http.StatusInternalServerError,
expectedMsg: "Internal server error",
},
{
name: "ServiceUnavailable",
builder: ServiceUnavailable,
expectedStatus: http.StatusServiceUnavailable,
expectedMsg: "Service unavailable",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Test New()
errResp := tt.builder.New()
if errResp.Status != tt.expectedStatus {
t.Errorf("expected status %d, got %d", tt.expectedStatus, errResp.Status)
}
if errResp.Message != tt.expectedMsg {
t.Errorf("expected message %q, got %q", tt.expectedMsg, errResp.Message)
}
if errResp.Err != nil {
t.Errorf("expected nil error, got %v", errResp.Err)
}
// Test GetStatus()
if tt.builder.GetStatus() != tt.expectedStatus {
t.Errorf("GetStatus() = %d, want %d", tt.builder.GetStatus(), tt.expectedStatus)
}
})
}
}
// TestErrorBuilderWithError tests the WithError method
func TestErrorBuilderWithError(t *testing.T) {
testErr := errors.New("test error")
wrappedErr := fmt.Errorf("wrapped: %w", testErr)
tests := []struct {
name string
builder *ErrorBuilder
err error
wantStatus int
wantMessage string
}{
{
name: "BadRequest with simple error",
builder: BadRequest,
err: testErr,
wantStatus: http.StatusBadRequest,
wantMessage: "Bad request",
},
{
name: "InternalError with wrapped error",
builder: InternalError,
err: wrappedErr,
wantStatus: http.StatusInternalServerError,
wantMessage: "Internal server error",
},
{
name: "GatewayTimeout with formatted error",
builder: GatewayTimeout,
err: fmt.Errorf("timeout after 30s: %w", testErr),
wantStatus: http.StatusGatewayTimeout,
wantMessage: "Gateway timeout",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
errResp := tt.builder.WithError(tt.err)
if errResp.Status != tt.wantStatus {
t.Errorf("Status = %d, want %d", errResp.Status, tt.wantStatus)
}
if errResp.Message != tt.wantMessage {
t.Errorf("Message = %q, want %q", errResp.Message, tt.wantMessage)
}
if errResp.Err != tt.err {
t.Errorf("Err = %v, want %v", errResp.Err, tt.err)
}
})
}
}
// TestErrorResponseToJSON tests JSON serialization
func TestErrorResponseToJSON(t *testing.T) {
tests := []struct {
name string
errResp *ErrorResponse
wantJSON string
}{
{
name: "without error",
errResp: BadRequest.New(),
wantJSON: `{"status":400,"message":"Bad request"}`,
},
{
name: "with simple error",
errResp: BadRequest.WithError(errors.New("invalid parameter")),
wantJSON: `{"status":400,"message":"Bad request","error":"invalid parameter"}`,
},
{
name: "with wrapped error",
errResp: InternalError.WithError(fmt.Errorf("failed to process: %w", errors.New("connection refused"))),
wantJSON: `{"status":500,"message":"Internal server error","error":"failed to process: connection refused"}`,
},
{
name: "NotFound without error",
errResp: RouteNotFound.New(),
wantJSON: `{"status":404,"message":"Route not found"}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotJSON := tt.errResp.ToJSON()
// Compare JSON structure
var got, want map[string]any
if err := json.Unmarshal(gotJSON, &got); err != nil {
t.Fatalf("failed to unmarshal got JSON: %v", err)
}
if err := json.Unmarshal([]byte(tt.wantJSON), &want); err != nil {
t.Fatalf("failed to unmarshal want JSON: %v", err)
}
if got["status"] != want["status"] {
t.Errorf("status = %v, want %v", got["status"], want["status"])
}
if got["message"] != want["message"] {
t.Errorf("message = %v, want %v", got["message"], want["message"])
}
if want["error"] != nil && got["error"] != want["error"] {
t.Errorf("error = %v, want %v", got["error"], want["error"])
}
})
}
}
// TestErrorResponseError tests the Error() method
func TestErrorResponseError(t *testing.T) {
tests := []struct {
name string
errResp *ErrorResponse
wantStr string
}{
{
name: "without error",
errResp: BadRequest.New(),
wantStr: "[400] Bad request",
},
{
name: "with simple error",
errResp: BadRequest.WithError(errors.New("invalid id")),
wantStr: "[400] Bad request: invalid id",
},
{
name: "with wrapped error",
errResp: InternalError.WithError(fmt.Errorf("database error: %w", errors.New("connection lost"))),
wantStr: "[500] Internal server error: database error: connection lost",
},
{
name: "503 with context",
errResp: ServiceUnavailable.WithError(fmt.Errorf("endpoint not found: %w", errors.New("no healthy hosts"))),
wantStr: "[503] Service unavailable: endpoint not found: no healthy hosts",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.errResp.Error()
if got != tt.wantStr {
t.Errorf("Error() = %q, want %q", got, tt.wantStr)
}
})
}
}
// TestAllErrorTypes verifies all predefined error types
func TestAllErrorTypes(t *testing.T) {
errorTypes := []struct {
name string
builder *ErrorBuilder
status int
}{
// 404 errors
{"RouteNotFound", RouteNotFound, http.StatusNotFound},
{"ServiceNotFound", ServiceNotFound, http.StatusNotFound},
{"APINotFound", APINotFound, http.StatusNotFound},
// 400 errors
{"BadRequest", BadRequest, http.StatusBadRequest},
// 401 errors
{"Unauthorized", Unauthorized, http.StatusUnauthorized},
// 403 errors
{"Forbidden", Forbidden, http.StatusForbidden},
// 405 errors
{"MethodNotAllowed", MethodNotAllowed, http.StatusMethodNotAllowed},
// 406 errors
{"NotAcceptable", NotAcceptable, http.StatusNotAcceptable},
// 429 errors
{"RateLimited", RateLimited, http.StatusTooManyRequests},
// 500 errors
{"InternalError", InternalError, http.StatusInternalServerError},
{"ConfigurationError", ConfigurationError, http.StatusInternalServerError},
// 502 errors
{"BadGateway", BadGateway, http.StatusBadGateway},
// 503 errors
{"ServiceUnavailable", ServiceUnavailable, http.StatusServiceUnavailable},
// 504 errors
{"GatewayTimeout", GatewayTimeout, http.StatusGatewayTimeout},
}
for _, tt := range errorTypes {
t.Run(tt.name, func(t *testing.T) {
if tt.builder == nil {
t.Fatal("error builder is nil")
}
if tt.builder.GetStatus() != tt.status {
t.Errorf("status = %d, want %d", tt.builder.GetStatus(), tt.status)
}
// Test that New() works
errResp := tt.builder.New()
if errResp.Status != tt.status {
t.Errorf("New().Status = %d, want %d", errResp.Status, tt.status)
}
// Test that WithError() works
testErr := errors.New("test")
errResp = tt.builder.WithError(testErr)
if errResp.Err != testErr {
t.Errorf("WithError().Err = %v, want %v", errResp.Err, testErr)
}
})
}
}
// TestErrorResponseJSONMarshaling tests edge cases in JSON marshaling
func TestErrorResponseJSONMarshaling(t *testing.T) {
t.Run("error with special characters", func(t *testing.T) {
errResp := BadRequest.WithError(errors.New(`error with "quotes" and \backslash`))
jsonBytes := errResp.ToJSON()
var result map[string]any
if err := json.Unmarshal(jsonBytes, &result); err != nil {
t.Fatalf("failed to unmarshal JSON: %v", err)
}
if result["error"] != `error with "quotes" and \backslash` {
t.Errorf("error field not properly escaped: %v", result["error"])
}
})
t.Run("nil error", func(t *testing.T) {
errResp := &ErrorResponse{
Status: http.StatusBadRequest,
Message: "Bad request",
Err: nil,
}
jsonBytes := errResp.ToJSON()
var result map[string]any
if err := json.Unmarshal(jsonBytes, &result); err != nil {
t.Fatalf("failed to unmarshal JSON: %v", err)
}
if _, hasError := result["error"]; hasError {
t.Error("error field should be omitted when Err is nil")
}
})
t.Run("WithError(nil) behavior", func(t *testing.T) {
errResp := BadRequest.WithError(nil)
jsonBytes := errResp.ToJSON()
var result map[string]any
if err := json.Unmarshal(jsonBytes, &result); err != nil {
t.Fatalf("failed to unmarshal JSON: %v", err)
}
// Verify no error field when error is nil
if _, hasError := result["error"]; hasError {
t.Error("WithError(nil) should not include error field in JSON")
}
// Verify basic fields are present
if result["status"] != float64(http.StatusBadRequest) {
t.Errorf("status = %v, want %v", result["status"], http.StatusBadRequest)
}
if result["message"] != "Bad request" {
t.Errorf("message = %v, want 'Bad request'", result["message"])
}
})
t.Run("empty message and zero status", func(t *testing.T) {
errResp := &ErrorResponse{
Status: 0,
Message: "",
Err: nil,
}
// Should not panic
jsonBytes := errResp.ToJSON()
var result map[string]any
if err := json.Unmarshal(jsonBytes, &result); err != nil {
t.Fatalf("failed to unmarshal JSON: %v", err)
}
if result["status"] != float64(0) {
t.Errorf("status = %v, want 0", result["status"])
}
if result["message"] != "" {
t.Errorf("message = %v, want empty string", result["message"])
}
// No error field expected
if _, hasError := result["error"]; hasError {
t.Error("error field should be omitted when Err is nil")
}
})
t.Run("Error() with empty message and zero status", func(t *testing.T) {
errResp := &ErrorResponse{
Status: 0,
Message: "",
Err: nil,
}
// Should not panic
got := errResp.Error()
want := "[0] "
if got != want {
t.Errorf("Error() = %q, want %q", got, want)
}
})
t.Run("Error() with empty message but has error", func(t *testing.T) {
errResp := &ErrorResponse{
Status: http.StatusInternalServerError,
Message: "",
Err: errors.New("internal error"),
}
// Should not panic
got := errResp.Error()
want := "[500] : internal error"
if got != want {
t.Errorf("Error() = %q, want %q", got, want)
}
})
t.Run("Error() with zero status and has error", func(t *testing.T) {
errResp := &ErrorResponse{
Status: 0,
Message: "Unknown error",
Err: errors.New("something went wrong"),
}
// Should not panic
got := errResp.Error()
want := "[0] Unknown error: something went wrong"
if got != want {
t.Errorf("Error() = %q, want %q", got, want)
}
})
}
// TestRecordMetric tests recording metrics to context
func TestRecordMetric(t *testing.T) {
req, err := http.NewRequest("GET", "http://example.com/test", nil)
if err != nil {
t.Fatalf("failed to create request: %v", err)
}
ctx := newTestHTTPContext(req)
// Record a metric
ctx.RecordMetric("test_metric", "counter", 1.0, map[string]string{
"label1": "value1",
"label2": "value2",
})
// Get all metrics
metrics := ctx.GetAllMetrics()
// Verify
if len(metrics) != 1 {
t.Errorf("expected 1 metric, got %d", len(metrics))
}
metric := metrics[0]
if metric.Name != "test_metric" {
t.Errorf("expected metric name 'test_metric', got %s", metric.Name)
}
if metric.Type != "counter" {
t.Errorf("expected metric type 'counter', got %s", metric.Type)
}
if metric.Value != 1.0 {
t.Errorf("expected metric value 1.0, got %f", metric.Value)
}
if len(metric.Labels) != 2 {
t.Errorf("expected 2 labels, got %d", len(metric.Labels))
}
if metric.Labels["label1"] != "value1" {
t.Errorf("expected label1=value1, got %s", metric.Labels["label1"])
}
}
// TestRecordMultipleMetrics tests recording multiple metrics
func TestRecordMultipleMetrics(t *testing.T) {
req, _ := http.NewRequest("GET", "http://example.com/test", nil)
ctx := newTestHTTPContext(req)
// Record multiple metrics
ctx.RecordMetric("metric1", "counter", 1.0, map[string]string{"type": "counter"})
ctx.RecordMetric("metric2", "histogram", 123.45, map[string]string{"type": "histogram"})
ctx.RecordMetric("metric3", "gauge", 42.0, map[string]string{"type": "gauge"})
// Get all metrics
metrics := ctx.GetAllMetrics()
// Verify
if len(metrics) != 3 {
t.Errorf("expected 3 metrics, got %d", len(metrics))
}
// Verify each metric
metricNames := make(map[string]bool)
for _, m := range metrics {
metricNames[m.Name] = true
}
if !metricNames["metric1"] || !metricNames["metric2"] || !metricNames["metric3"] {
t.Error("not all metrics were recorded")
}
}
// TestClearMetrics tests clearing metrics
func TestClearMetrics(t *testing.T) {
req, _ := http.NewRequest("GET", "http://example.com/test", nil)
ctx := newTestHTTPContext(req)
// Record metrics
ctx.RecordMetric("test1", "counter", 1.0, nil)
ctx.RecordMetric("test2", "counter", 2.0, nil)
// Verify metrics exist
metrics := ctx.GetAllMetrics()
if len(metrics) != 2 {
t.Errorf("expected 2 metrics before clear, got %d", len(metrics))
}
// Clear metrics
ctx.ClearMetrics()
// Verify metrics are cleared
metrics = ctx.GetAllMetrics()
if len(metrics) != 0 {
t.Errorf("expected 0 metrics after clear, got %d", len(metrics))
}
}
// TestMetricLabelIsolation tests that modifying labels doesn't affect stored metrics
func TestMetricLabelIsolation(t *testing.T) {
req, _ := http.NewRequest("GET", "http://example.com/test", nil)
ctx := newTestHTTPContext(req)
// Create labels
labels := map[string]string{
"key": "original",
}
// Record metric
ctx.RecordMetric("test_metric", "counter", 1.0, labels)
// Modify original labels
labels["key"] = "modified"
labels["new_key"] = "new_value"
// Get metrics
metrics := ctx.GetAllMetrics()
// Verify the stored metric labels are not affected
if len(metrics) != 1 {
t.Fatalf("expected 1 metric, got %d", len(metrics))
}
storedLabels := metrics[0].Labels
if storedLabels["key"] != "original" {
t.Errorf("expected label to be 'original', got %s", storedLabels["key"])
}
if _, exists := storedLabels["new_key"]; exists {
t.Error("new_key should not exist in stored labels")
}
}
// TestRecordMetricWithNilLabels tests recording metrics with nil labels
func TestRecordMetricWithNilLabels(t *testing.T) {
req, _ := http.NewRequest("GET", "http://example.com/test", nil)
ctx := newTestHTTPContext(req)
// Record metric with nil labels
ctx.RecordMetric("test_metric", "counter", 1.0, nil)
// Get metrics
metrics := ctx.GetAllMetrics()
// Verify
if len(metrics) != 1 {
t.Fatalf("expected 1 metric, got %d", len(metrics))
}
if metrics[0].Labels == nil {
t.Error("labels should not be nil, should be empty map")
}
if len(metrics[0].Labels) != 0 {
t.Errorf("expected 0 labels, got %d", len(metrics[0].Labels))
}
}
// TestRecordMetricWithEmptyLabels tests recording metrics with empty labels
func TestRecordMetricWithEmptyLabels(t *testing.T) {
req, _ := http.NewRequest("GET", "http://example.com/test", nil)
ctx := newTestHTTPContext(req)
// Record metric with empty labels
ctx.RecordMetric("test_metric", "counter", 1.0, map[string]string{})
// Get metrics
metrics := ctx.GetAllMetrics()
// Verify
if len(metrics) != 1 {
t.Fatalf("expected 1 metric, got %d", len(metrics))
}
if len(metrics[0].Labels) != 0 {
t.Errorf("expected 0 labels, got %d", len(metrics[0].Labels))
}
}
// TestGetAllMetricsReturnsCopy tests that GetAllMetrics returns a copy
func TestGetAllMetricsReturnsCopy(t *testing.T) {
req, _ := http.NewRequest("GET", "http://example.com/test", nil)
ctx := newTestHTTPContext(req)
// Record a metric
ctx.RecordMetric("test1", "counter", 1.0, map[string]string{"key": "value"})
// Get metrics twice
metrics1 := ctx.GetAllMetrics()
metrics2 := ctx.GetAllMetrics()
// Verify they are different slices (not the same reference)
if &metrics1[0] == &metrics2[0] {
t.Error("GetAllMetrics should return a copy, not the same reference")
}
// Verify contents are the same
if metrics1[0].Name != metrics2[0].Name {
t.Error("metric contents should be the same")
}
}
// TestConcurrentMetricRecording tests concurrent metric recording
func TestConcurrentMetricRecording(t *testing.T) {
req, _ := http.NewRequest("GET", "http://example.com/test", nil)
ctx := newTestHTTPContext(req)
// Record metrics concurrently
done := make(chan bool)
for i := 0; i < 100; i++ {
go func(id int) {
ctx.RecordMetric(
fmt.Sprintf("metric_%d", id),
"counter",
float64(id),
map[string]string{"id": fmt.Sprintf("%d", id)},
)
done <- true
}(i)
}
// Wait for all goroutines
for i := 0; i < 100; i++ {
<-done
}
// Verify all metrics were recorded
metrics := ctx.GetAllMetrics()
if len(metrics) != 100 {
t.Errorf("expected 100 metrics, got %d", len(metrics))
}
}
// TestConcurrentGetAllMetrics tests concurrent calls to GetAllMetrics
func TestConcurrentGetAllMetrics(t *testing.T) {
req, _ := http.NewRequest("GET", "http://example.com/test", nil)
ctx := newTestHTTPContext(req)
// Pre-populate with metrics
for i := 0; i < 50; i++ {
ctx.RecordMetric(fmt.Sprintf("metric_%d", i), "counter", float64(i), nil)
}
// Concurrently call GetAllMetrics
done := make(chan bool)
for i := 0; i < 100; i++ {
go func() {
metrics := ctx.GetAllMetrics()
if len(metrics) != 50 {
t.Errorf("expected 50 metrics, got %d", len(metrics))
}
done <- true
}()
}
// Wait for all goroutines
for i := 0; i < 100; i++ {
<-done
}
}
// TestMetricDataTypes tests different metric types
func TestMetricDataTypes(t *testing.T) {
req, _ := http.NewRequest("GET", "http://example.com/test", nil)
ctx := newTestHTTPContext(req)
tests := []struct {
name string
metricName string
metricType string
value float64
}{
{"counter", "test_counter", "counter", 1.0},
{"histogram", "test_histogram", "histogram", 123.45},
{"gauge", "test_gauge", "gauge", 42.0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx.ClearMetrics() // Clear before each test
ctx.RecordMetric(tt.metricName, tt.metricType, tt.value, nil)
metrics := ctx.GetAllMetrics()
if len(metrics) != 1 {
t.Fatalf("expected 1 metric, got %d", len(metrics))
}
if metrics[0].Name != tt.metricName {
t.Errorf("expected name %s, got %s", tt.metricName, metrics[0].Name)
}
if metrics[0].Type != tt.metricType {
t.Errorf("expected type %s, got %s", tt.metricType, metrics[0].Type)
}
if metrics[0].Value != tt.value {
t.Errorf("expected value %f, got %f", tt.value, metrics[0].Value)
}
})
}
}
// BenchmarkRecordMetric benchmarks recording metrics
func BenchmarkRecordMetric(b *testing.B) {
req, _ := http.NewRequest("GET", "http://example.com/test", nil)
ctx := newTestHTTPContext(req)
labels := map[string]string{
"method": "GET",
"status": "200",
"path": "/api/test",
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
ctx.RecordMetric("benchmark_metric", "counter", 1.0, labels)
}
}
// BenchmarkGetAllMetrics benchmarks getting all metrics
func BenchmarkGetAllMetrics(b *testing.B) {
req, _ := http.NewRequest("GET", "http://example.com/test", nil)
ctx := newTestHTTPContext(req)
// Pre-populate with metrics
for i := 0; i < 10; i++ {
ctx.RecordMetric(
fmt.Sprintf("metric_%d", i),
"counter",
float64(i),
map[string]string{"id": fmt.Sprintf("%d", i)},
)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = ctx.GetAllMetrics()
}
}