package server import ( "context" "errors" "net/http" "net/http/httptest" "testing" "time" "proxy-pool/internal/config" "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 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) } }