proxy-pool/internal/gateway/server/bootstrap_test.go

217 lines
7.6 KiB
Go

package server
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"testing"
"time"
"proxy-pool/internal/config"
"proxy-pool/internal/domain/clientpolicy"
"proxy-pool/internal/gateway/policy"
"proxy-pool/internal/platform/httpsecurity"
)
func TestBuildProtectionFromListenerConfig(t *testing.T) {
t.Parallel()
protection, err := BuildProtection(config.Listener{
Access: config.Access{AllowCIDRs: []string{"198.51.100.0/24"}},
Auth: config.Auth{
Mode: "usernamePassword",
Username: "client",
Password: "secret",
},
Limits: config.Limits{RequestsPerMinute: 10, RequestsPerMinutePerClient: 2},
})
if err != nil {
t.Fatalf("BuildProtection() error = %v", err)
}
request := httptest.NewRequest(http.MethodGet, "http://example.test", nil)
request.RemoteAddr = "198.51.100.8:1234"
request.Header.Set("Proxy-Authorization", "Basic Y2xpZW50OnNlY3JldA==")
for name, guard := range map[string]Guard{
"auth": protection.Auth, "access": protection.Access, "admission": protection.Admission,
} {
if err := guard.Check(context.Background(), request); err != nil {
t.Fatalf("%s guard error = %v", name, err)
}
}
}
func TestBuildProtectionSupportsBearerProxyAuthentication(t *testing.T) {
t.Parallel()
protection, err := BuildProtection(config.Listener{Auth: config.Auth{Mode: "bearer", Token: "proxy-token"}})
if err != nil {
t.Fatalf("BuildProtection() error = %v", err)
}
request := httptest.NewRequest(http.MethodGet, "http://example.test", nil)
request.RemoteAddr = "198.51.100.8:1234"
request.Header.Set("Proxy-Authorization", "Bearer proxy-token")
if err := protection.Auth.Check(context.Background(), request); err != nil {
t.Fatalf("auth.Check() error = %v", err)
}
request.Header.Set("Proxy-Authorization", "Bearer wrong")
err = protection.Auth.Check(context.Background(), request)
var securityError *httpsecurity.HTTPError
if !errors.As(err, &securityError) || securityError.StatusCode != http.StatusProxyAuthRequired {
t.Fatalf("auth.Check(wrong) error = %T %v", err, err)
}
recorder := httptest.NewRecorder()
writeGatewayError(recorder, err)
if recorder.Code != http.StatusProxyAuthRequired || recorder.Header().Get("Proxy-Authenticate") == "" {
t.Fatalf("gateway response = status %d headers %v", recorder.Code, recorder.Header())
}
}
func TestBuildProtectionAppliesCredentialRequestRateLimit(t *testing.T) {
t.Parallel()
protection, err := BuildProtection(config.Listener{
Auth: config.Auth{
Mode: "bearer", Token: "gateway-token",
ClientPolicy: clientpolicy.Policy{RequestsPerMinute: 1},
},
})
if err != nil {
t.Fatalf("BuildProtection() error = %v", err)
}
for attempt := range 2 {
request := httptest.NewRequest(http.MethodGet, "http://example.test", nil)
request.RemoteAddr = "198.51.100.8:1234"
request.Header.Set("Proxy-Authorization", "Bearer gateway-token")
if err := protection.Auth.Check(context.Background(), request); err != nil {
t.Fatalf("Auth.Check(%d) error = %v", attempt, err)
}
err := protection.Admission.Check(context.Background(), request)
if attempt == 0 && err != nil {
t.Fatalf("Admission.Check(first) error = %v", err)
}
if attempt == 1 {
var admissionError *HTTPError
if !errors.As(err, &admissionError) || admissionError.StatusCode != http.StatusTooManyRequests {
t.Fatalf("Admission.Check(second) error = %T %v, want 429", err, err)
}
}
}
}
func TestBuildProtectionAnyPreservesIPWhitelistRejectionIndependentOfOrder(t *testing.T) {
t.Parallel()
methods := [][]config.AuthMethod{
{
{Mode: "ipWhitelist", CIDRs: []string{"10.0.0.0/8"}},
{Mode: "apiKey", Header: "X-Proxy-Key", Value: "secret"},
},
{
{Mode: "apiKey", Header: "X-Proxy-Key", Value: "secret"},
{Mode: "ipWhitelist", CIDRs: []string{"10.0.0.0/8"}},
},
}
for index, configuredMethods := range methods {
protection, err := BuildProtection(config.Listener{
Auth: config.Auth{Mode: "any", Methods: configuredMethods},
})
if err != nil {
t.Fatalf("BuildProtection(%d) error = %v", index, err)
}
request := httptest.NewRequest(http.MethodGet, "http://example.test", nil)
request.RemoteAddr = "198.51.100.8:1234"
err = protection.Auth.Check(context.Background(), request)
recorder := httptest.NewRecorder()
writeGatewayError(recorder, err)
if recorder.Code != http.StatusForbidden {
t.Fatalf("method order %d status = %d, want 403", index, recorder.Code)
}
}
}
func TestTargetPolicyFromOmittedConfigDefaultsToDeny(t *testing.T) {
t.Parallel()
targets, err := TargetPolicyFromListener(config.Listener{})
if err != nil {
t.Fatalf("TargetPolicyFromListener() error = %v", err)
}
if _, err := targets.EvaluateURL(context.Background(), "http://127.0.0.1/"); !errors.Is(err, policy.ErrTargetDenied) {
t.Fatalf("EvaluateURL(loopback) error = %v, want ErrTargetDenied", err)
}
if _, err := targets.EvaluateConnectAuthority(context.Background(), "8.8.8.8:22"); !errors.Is(err, policy.ErrTargetDenied) {
t.Fatalf("EvaluateConnectAuthority(port 22) error = %v, want ErrTargetDenied", err)
}
}
func TestTargetPolicyFromListenerMapsAllowedPorts(t *testing.T) {
t.Parallel()
targets, err := TargetPolicyFromListener(config.Listener{
DestinationPolicy: config.DestinationPolicy{AllowedPorts: []uint16{8443}},
})
if err != nil {
t.Fatalf("TargetPolicyFromListener() error = %v", err)
}
if _, err := targets.EvaluateConnectAuthority(context.Background(), "8.8.8.8:8443"); err != nil {
t.Fatalf("EvaluateConnectAuthority(port 8443) error = %v", err)
}
if _, err := targets.EvaluateConnectAuthority(context.Background(), "8.8.8.8:443"); !errors.Is(err, policy.ErrTargetDenied) {
t.Fatalf("EvaluateConnectAuthority(port 443) error = %v, want ErrTargetDenied", err)
}
}
func TestTargetPolicyFromPartialConfigKeepsOmittedCategoriesDenied(t *testing.T) {
t.Parallel()
deny := true
targets, err := TargetPolicyFromListener(config.Listener{
DestinationPolicy: config.DestinationPolicy{DenyPrivateNetworks: &deny},
})
if err != nil {
t.Fatalf("TargetPolicyFromListener() error = %v", err)
}
for _, target := range []string{"http://127.0.0.1/", "http://169.254.10.20/"} {
if _, err := targets.EvaluateURL(context.Background(), target); !errors.Is(err, policy.ErrTargetDenied) {
t.Fatalf("EvaluateURL(%q) error = %v, want ErrTargetDenied", target, err)
}
}
}
func TestTargetPolicyFromListenerAllowsOnlyExplicitlyFalseCategory(t *testing.T) {
t.Parallel()
allow := false
targets, err := TargetPolicyFromListener(config.Listener{
DestinationPolicy: config.DestinationPolicy{DenyLoopback: &allow},
})
if err != nil {
t.Fatalf("TargetPolicyFromListener() error = %v", err)
}
if _, err := targets.EvaluateURL(context.Background(), "http://127.0.0.1/"); err != nil {
t.Fatalf("EvaluateURL(loopback) error = %v", err)
}
for _, target := range []string{"http://10.0.0.1/", "http://169.254.10.20/"} {
if _, err := targets.EvaluateURL(context.Background(), target); !errors.Is(err, policy.ErrTargetDenied) {
t.Fatalf("EvaluateURL(%q) error = %v, want ErrTargetDenied", target, err)
}
}
}
func TestConfigFromListenerMapsRetryAndConcurrency(t *testing.T) {
t.Parallel()
result := ConfigFromListener(config.Listener{
Limits: config.Limits{MaxConcurrentConnections: 123},
Retry: config.Retry{MaxAttempts: 2, RetryMethods: []string{"GET", "HEAD"}},
StickySession: config.StickySession{
Enabled: true, Header: "X-Proxy-Session", TTL: config.Duration(time.Minute), MaxEntries: 456,
},
})
if result.MaxConcurrentRequests != 123 || result.MaxAttempts != 2 || len(result.RetryMethods) != 2 ||
result.StickySession.Header != "X-Proxy-Session" || result.StickySession.TTL != time.Minute ||
result.StickySession.MaxEntries != 456 {
t.Fatalf("handler config = %+v", result)
}
}