chore: clean .gitignore cache
This commit is contained in:
@@ -1,489 +0,0 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestTestURLConnectivity_EnhancedSSRF tests additional SSRF attack vectors.
|
||||
// ENHANCEMENT: Comprehensive SSRF testing per Supervisor review
|
||||
func TestTestURLConnectivity_EnhancedSSRF(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
url string
|
||||
shouldFail bool
|
||||
errContains string
|
||||
}{
|
||||
// Cloud Metadata Endpoints
|
||||
{
|
||||
name: "AWS metadata endpoint",
|
||||
url: "http://169.254.169.254/latest/meta-data/",
|
||||
shouldFail: true,
|
||||
errContains: "private IP",
|
||||
},
|
||||
{
|
||||
name: "GCP metadata endpoint",
|
||||
url: "http://metadata.google.internal/computeMetadata/v1/",
|
||||
shouldFail: true,
|
||||
errContains: "DNS resolution failed", // Will fail to resolve
|
||||
},
|
||||
{
|
||||
name: "Azure metadata endpoint",
|
||||
url: "http://169.254.169.254/metadata/instance",
|
||||
shouldFail: true,
|
||||
errContains: "private IP",
|
||||
},
|
||||
|
||||
// RFC 1918 Private Networks
|
||||
{
|
||||
name: "Private 10.0.0.0/8",
|
||||
url: "http://10.0.0.1/admin",
|
||||
shouldFail: true,
|
||||
errContains: "private IP",
|
||||
},
|
||||
{
|
||||
name: "Private 172.16.0.0/12",
|
||||
url: "http://172.16.0.1/admin",
|
||||
shouldFail: true,
|
||||
errContains: "private IP",
|
||||
},
|
||||
{
|
||||
name: "Private 192.168.0.0/16",
|
||||
url: "http://192.168.1.1/admin",
|
||||
shouldFail: true,
|
||||
errContains: "private IP",
|
||||
},
|
||||
|
||||
// Loopback Addresses
|
||||
{
|
||||
name: "IPv4 loopback",
|
||||
url: "http://127.0.0.1:6379/",
|
||||
shouldFail: true,
|
||||
errContains: "private IP",
|
||||
},
|
||||
{
|
||||
name: "IPv6 loopback",
|
||||
url: "http://[::1]:6379/",
|
||||
shouldFail: true,
|
||||
errContains: "private IP",
|
||||
},
|
||||
|
||||
// Link-Local Addresses
|
||||
{
|
||||
name: "Link-local IPv4",
|
||||
url: "http://169.254.1.1/",
|
||||
shouldFail: true,
|
||||
errContains: "private IP",
|
||||
},
|
||||
{
|
||||
name: "Link-local IPv6",
|
||||
url: "http://[fe80::1]/",
|
||||
shouldFail: true,
|
||||
errContains: "private IP",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
reachable, _, err := TestURLConnectivity(tt.url)
|
||||
|
||||
if tt.shouldFail {
|
||||
if err == nil {
|
||||
t.Errorf("Expected test to fail, but it succeeded")
|
||||
} else if !strings.Contains(err.Error(), tt.errContains) {
|
||||
t.Errorf("Expected error containing '%s', got: %s", tt.errContains, err.Error())
|
||||
}
|
||||
if reachable {
|
||||
t.Errorf("Expected reachable=false, got true")
|
||||
}
|
||||
} else if err != nil {
|
||||
t.Errorf("Expected test to succeed, but got error: %s", err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestTestURLConnectivity_RedirectValidation tests that redirects are properly validated.
|
||||
// ENHANCEMENT: Critical test per Supervisor review - all redirects must be validated
|
||||
func TestTestURLConnectivity_RedirectValidation(t *testing.T) {
|
||||
// Create test servers
|
||||
privateServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("Private server"))
|
||||
}))
|
||||
defer privateServer.Close()
|
||||
|
||||
t.Run("Redirect to private IP should be blocked", func(t *testing.T) {
|
||||
// Note: This test requires actual redirect validation to work
|
||||
// The validateRedirectTarget function will validate the Location header
|
||||
// For now, we skip this test as it requires complex mock setup
|
||||
t.Skip("Redirect validation test requires complex HTTP client mocking")
|
||||
})
|
||||
|
||||
t.Run("Too many redirects should be blocked", func(t *testing.T) {
|
||||
// Create a server that redirects to itself multiple times
|
||||
redirectCount := 0
|
||||
var redirectServerURL string
|
||||
redirectServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
redirectCount++
|
||||
if redirectCount < 5 {
|
||||
http.Redirect(w, r, redirectServerURL+fmt.Sprintf("/%d", redirectCount), http.StatusFound)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
}))
|
||||
defer redirectServer.Close()
|
||||
redirectServerURL = redirectServer.URL
|
||||
|
||||
transport := redirectServer.Client().Transport
|
||||
reachable, _, err := testURLConnectivity(redirectServerURL, withAllowLocalhostForTesting(), withTransportForTesting(transport))
|
||||
|
||||
// Should fail due to too many redirects (max 2)
|
||||
if err == nil {
|
||||
t.Errorf("Expected too many redirects to fail, but it succeeded")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "redirect") {
|
||||
t.Errorf("Expected error about redirects, got: %s", err.Error())
|
||||
}
|
||||
if reachable {
|
||||
t.Errorf("Expected reachable=false, got true")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestTestURLConnectivity_UnicodeHomograph tests Unicode homograph attack prevention.
|
||||
// ENHANCEMENT: Tests for internationalized domain name attacks
|
||||
func TestTestURLConnectivity_UnicodeHomograph(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
url string
|
||||
}{
|
||||
{
|
||||
name: "Cyrillic homograph",
|
||||
url: "https://gооgle.com", // Uses Cyrillic 'о' instead of Latin 'o'
|
||||
},
|
||||
{
|
||||
name: "Mixed script attack",
|
||||
url: "https://раypal.com", // Uses Cyrillic 'а' and 'y'
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// These should fail DNS resolution since they're not real domains
|
||||
reachable, _, err := TestURLConnectivity(tt.url)
|
||||
if err == nil {
|
||||
t.Logf("Warning: homograph domain %s resolved - may indicate IDN issue", tt.url)
|
||||
}
|
||||
if reachable {
|
||||
t.Errorf("Homograph domain %s appears reachable - security concern", tt.url)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestTestURLConnectivity_LongHostname tests extremely long hostname handling.
|
||||
// ENHANCEMENT: DoS prevention via hostname length validation
|
||||
func TestTestURLConnectivity_LongHostname(t *testing.T) {
|
||||
// Create hostname exceeding RFC 1035 limit (253 chars)
|
||||
longHostname := strings.Repeat("a", 254) + ".com"
|
||||
url := "https://" + longHostname + "/path"
|
||||
|
||||
reachable, _, err := TestURLConnectivity(url)
|
||||
if err == nil {
|
||||
t.Errorf("Expected long hostname to be rejected, but it was accepted")
|
||||
}
|
||||
if reachable {
|
||||
t.Errorf("Expected reachable=false for long hostname, got true")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTestURLConnectivity_RequestTracingHeaders tests that tracing headers are added.
|
||||
// ENHANCEMENT: Verifies request tracing per Supervisor review
|
||||
func TestTestURLConnectivity_RequestTracingHeaders(t *testing.T) {
|
||||
var capturedHeaders http.Header
|
||||
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
capturedHeaders = r.Header.Clone()
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer testServer.Close()
|
||||
|
||||
transport := testServer.Client().Transport
|
||||
_, _, err := testURLConnectivity(testServer.URL, withAllowLocalhostForTesting(), withTransportForTesting(transport))
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %s", err)
|
||||
}
|
||||
|
||||
// Verify headers were set
|
||||
if capturedHeaders.Get("User-Agent") != "Charon-Health-Check/1.0" {
|
||||
t.Errorf("Expected User-Agent header, got: %s", capturedHeaders.Get("User-Agent"))
|
||||
}
|
||||
if capturedHeaders.Get("X-Charon-Request-Type") != "url-connectivity-test" {
|
||||
t.Errorf("Expected X-Charon-Request-Type header, got: %s", capturedHeaders.Get("X-Charon-Request-Type"))
|
||||
}
|
||||
if capturedHeaders.Get("X-Request-ID") == "" {
|
||||
t.Errorf("Expected X-Request-ID header to be set")
|
||||
}
|
||||
if !strings.HasPrefix(capturedHeaders.Get("X-Request-ID"), "test-") {
|
||||
t.Errorf("Expected X-Request-ID to start with 'test-', got: %s", capturedHeaders.Get("X-Request-ID"))
|
||||
}
|
||||
}
|
||||
|
||||
// TestTestURLConnectivity_MetricsIntegration tests that metrics are recorded.
|
||||
// ENHANCEMENT: Validates metrics collection per Supervisor review
|
||||
func TestTestURLConnectivity_MetricsIntegration(t *testing.T) {
|
||||
// This test verifies that metrics functions are called
|
||||
// Full metrics validation requires integration tests with Prometheus
|
||||
|
||||
t.Run("Valid URL records metrics", func(t *testing.T) {
|
||||
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer testServer.Close()
|
||||
|
||||
transport := testServer.Client().Transport
|
||||
reachable, latency, err := testURLConnectivity(testServer.URL, withAllowLocalhostForTesting(), withTransportForTesting(transport))
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %s", err)
|
||||
}
|
||||
if !reachable {
|
||||
t.Errorf("Expected reachable=true, got false")
|
||||
}
|
||||
if latency <= 0 {
|
||||
t.Errorf("Expected positive latency, got %f", latency)
|
||||
}
|
||||
// Metrics recorded: URLValidation (allowed), URLTestDuration
|
||||
})
|
||||
|
||||
t.Run("Blocked URL records metrics", func(t *testing.T) {
|
||||
reachable, _, err := TestURLConnectivity("http://127.0.0.1:6379/")
|
||||
|
||||
if err == nil {
|
||||
t.Errorf("Expected private IP to be blocked")
|
||||
}
|
||||
if reachable {
|
||||
t.Errorf("Expected reachable=false, got true")
|
||||
}
|
||||
// Metrics recorded: SSRFBlock, URLValidation (blocked)
|
||||
})
|
||||
|
||||
t.Run("Invalid URL records metrics", func(t *testing.T) {
|
||||
reachable, _, err := TestURLConnectivity("not-a-valid-url")
|
||||
|
||||
if err == nil {
|
||||
t.Errorf("Expected invalid URL to fail")
|
||||
}
|
||||
if reachable {
|
||||
t.Errorf("Expected reachable=false, got true")
|
||||
}
|
||||
// Metrics recorded: URLValidation (error, unsupported_scheme)
|
||||
})
|
||||
}
|
||||
|
||||
// TestValidateRedirectTarget tests the redirect validation function directly.
|
||||
// ENHANCEMENT: Direct unit tests for redirect validation per Phase 2 requirements
|
||||
func TestValidateRedirectTarget(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
url string
|
||||
viaCount int
|
||||
shouldErr bool
|
||||
errContains string
|
||||
viaURL string
|
||||
allowLocal bool
|
||||
}{
|
||||
{
|
||||
name: "Localhost redirect allowed",
|
||||
url: "http://localhost/path",
|
||||
viaCount: 0,
|
||||
shouldErr: false,
|
||||
allowLocal: true,
|
||||
},
|
||||
{
|
||||
name: "127.0.0.1 redirect allowed",
|
||||
url: "http://127.0.0.1:8080/path",
|
||||
viaCount: 0,
|
||||
shouldErr: false,
|
||||
allowLocal: true,
|
||||
},
|
||||
{
|
||||
name: "IPv6 loopback allowed",
|
||||
url: "http://[::1]:8080/path",
|
||||
viaCount: 0,
|
||||
shouldErr: false,
|
||||
allowLocal: true,
|
||||
},
|
||||
{
|
||||
name: "Too many redirects",
|
||||
url: "http://localhost/path",
|
||||
viaCount: 2,
|
||||
shouldErr: true,
|
||||
errContains: "too many redirects",
|
||||
allowLocal: true,
|
||||
},
|
||||
{
|
||||
name: "Three redirects",
|
||||
url: "http://localhost/path",
|
||||
viaCount: 3,
|
||||
shouldErr: true,
|
||||
errContains: "too many redirects",
|
||||
allowLocal: true,
|
||||
},
|
||||
{
|
||||
name: "Scheme downgrade blocked (https -> http)",
|
||||
url: "http://localhost/next",
|
||||
viaURL: "https://localhost/start",
|
||||
viaCount: 1,
|
||||
shouldErr: true,
|
||||
errContains: "scheme change blocked",
|
||||
allowLocal: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create a request for the redirect target
|
||||
req, err := http.NewRequest("GET", tt.url, http.NoBody)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create request: %v", err)
|
||||
}
|
||||
|
||||
// Create via slice (previous requests)
|
||||
via := make([]*http.Request, 0, tt.viaCount)
|
||||
if tt.viaCount > 0 {
|
||||
viaURL := tt.viaURL
|
||||
if viaURL == "" {
|
||||
viaURL = "http://localhost/prev"
|
||||
}
|
||||
prevReq, prevErr := http.NewRequest("GET", viaURL, http.NoBody)
|
||||
if prevErr != nil {
|
||||
t.Fatalf("Failed to create via request: %v", prevErr)
|
||||
}
|
||||
via = append(via, prevReq)
|
||||
for i := 1; i < tt.viaCount; i++ {
|
||||
via = append(via, prevReq)
|
||||
}
|
||||
}
|
||||
|
||||
err = validateRedirectTargetStrict(req, via, 2, true, tt.allowLocal)
|
||||
|
||||
if tt.shouldErr {
|
||||
if err == nil {
|
||||
t.Errorf("Expected error, got nil")
|
||||
} else if tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) {
|
||||
t.Errorf("Expected error containing '%s', got: %s", tt.errContains, err.Error())
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestTestURLConnectivity_AuditLogging tests that audit logging is integrated.
|
||||
// ENHANCEMENT: Validates audit logging integration per Phase 3 requirements
|
||||
func TestTestURLConnectivity_AuditLogging(t *testing.T) {
|
||||
// Note: In production, audit logs go to the configured logger.
|
||||
// These tests verify the code paths execute without panic.
|
||||
|
||||
t.Run("Invalid URL format logs audit event", func(t *testing.T) {
|
||||
// This should trigger audit logging for invalid format
|
||||
reachable, _, err := TestURLConnectivity("://invalid")
|
||||
|
||||
if err == nil {
|
||||
t.Errorf("Expected error for invalid URL")
|
||||
}
|
||||
if reachable {
|
||||
t.Errorf("Expected reachable=false")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Invalid scheme logs audit event", func(t *testing.T) {
|
||||
// This should trigger audit logging for unsupported scheme
|
||||
reachable, _, err := TestURLConnectivity("ftp://example.com/file")
|
||||
|
||||
if err == nil {
|
||||
t.Errorf("Expected error for unsupported scheme")
|
||||
}
|
||||
if reachable {
|
||||
t.Errorf("Expected reachable=false")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Private IP logs SSRF block audit event", func(t *testing.T) {
|
||||
// This should trigger SSRF block audit logging
|
||||
reachable, _, err := TestURLConnectivity("http://10.0.0.1/admin")
|
||||
|
||||
if err == nil {
|
||||
t.Errorf("Expected error for private IP")
|
||||
}
|
||||
if reachable {
|
||||
t.Errorf("Expected reachable=false")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Metadata endpoint logs SSRF block audit event", func(t *testing.T) {
|
||||
// This should trigger metadata endpoint block audit logging
|
||||
reachable, _, err := TestURLConnectivity("http://169.254.169.254/latest/meta-data/")
|
||||
|
||||
if err == nil {
|
||||
t.Errorf("Expected error for metadata endpoint")
|
||||
}
|
||||
if reachable {
|
||||
t.Errorf("Expected reachable=false")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Valid URL with mock transport logs success", func(t *testing.T) {
|
||||
// Create a test server
|
||||
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer testServer.Close()
|
||||
|
||||
// Note: With mock transport, audit logging is skipped (isTestMode=true)
|
||||
// This test verifies the code path doesn't panic
|
||||
transport := testServer.Client().Transport
|
||||
reachable, _, err := testURLConnectivity(testServer.URL, withAllowLocalhostForTesting(), withTransportForTesting(transport))
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %s", err)
|
||||
}
|
||||
if !reachable {
|
||||
t.Errorf("Expected reachable=true")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestTestURLConnectivity_RequestIDConsistency tests that request ID is consistent.
|
||||
// ENHANCEMENT: Validates request tracing per Phase 3 requirements
|
||||
func TestTestURLConnectivity_RequestIDConsistency(t *testing.T) {
|
||||
var capturedRequestID string
|
||||
|
||||
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
capturedRequestID = r.Header.Get("X-Request-ID")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer testServer.Close()
|
||||
|
||||
transport := testServer.Client().Transport
|
||||
_, _, err := testURLConnectivity(testServer.URL, withAllowLocalhostForTesting(), withTransportForTesting(transport))
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %s", err)
|
||||
}
|
||||
|
||||
if capturedRequestID == "" {
|
||||
t.Error("X-Request-ID header was not set")
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(capturedRequestID, "test-") {
|
||||
t.Errorf("X-Request-ID should start with 'test-', got: %s", capturedRequestID)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user