mirror of
https://github.com/karust/openserp.git
synced 2026-08-05 16:53:54 +08:00
627 lines
18 KiB
Go
627 lines
18 KiB
Go
package core
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
socks5 "github.com/armon/go-socks5"
|
|
xcontext "golang.org/x/net/context"
|
|
)
|
|
|
|
type timeoutTestError struct{}
|
|
|
|
func (timeoutTestError) Error() string { return "i/o timeout" }
|
|
func (timeoutTestError) Timeout() bool { return true }
|
|
func (timeoutTestError) Temporary() bool { return true }
|
|
|
|
func TestNormalizeProxyURL(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
raw string
|
|
want string
|
|
wantErr bool
|
|
}{
|
|
{name: "empty", raw: "", want: ""},
|
|
{name: "http", raw: "http://127.0.0.1:8080", want: "http://127.0.0.1:8080"},
|
|
{name: "https", raw: "https://127.0.0.1:8443", want: "https://127.0.0.1:8443"},
|
|
{name: "socks5", raw: "socks5://127.0.0.1:1080", want: "socks5://127.0.0.1:1080"},
|
|
{name: "socks5h upper", raw: "SOCKS5H://127.0.0.1:1080", want: "socks5h://127.0.0.1:1080"},
|
|
{name: "missing scheme", raw: "127.0.0.1:8080", wantErr: true},
|
|
{name: "missing host", raw: "http://", wantErr: true},
|
|
{name: "unsupported scheme", raw: "ftp://127.0.0.1:21", wantErr: true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := NormalizeProxyURL(tt.raw)
|
|
if tt.wantErr {
|
|
if err == nil {
|
|
t.Fatalf("expected error for %q", tt.raw)
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if got != tt.want {
|
|
t.Fatalf("expected %q, got %q", tt.want, got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNormalizeProxiesConfigDefaultsAndDeduplicates(t *testing.T) {
|
|
cfg, err := NormalizeProxiesConfig(ProxiesConfig{
|
|
Global: " HTTP://proxy-global:8080 ",
|
|
Entries: []ProxyEntryConfig{
|
|
{URL: " http://proxy-one:8080 ", Tags: []string{"default", "us"}},
|
|
{URL: "http://proxy-one:8080", Tags: []string{"de", "us"}},
|
|
{URL: "socks5://proxy-two:1080", Tags: []string{"default"}},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("normalize proxies config: %v", err)
|
|
}
|
|
|
|
if cfg.Global != "http://proxy-global:8080" {
|
|
t.Fatalf("expected normalized global proxy, got %q", cfg.Global)
|
|
}
|
|
if cfg.Health.FailureThreshold != DefaultProxyFailureThreshold {
|
|
t.Fatalf("expected default failure threshold %d, got %d", DefaultProxyFailureThreshold, cfg.Health.FailureThreshold)
|
|
}
|
|
if len(cfg.Entries) != 2 {
|
|
t.Fatalf("expected 2 deduplicated entries, got %d", len(cfg.Entries))
|
|
}
|
|
if cfg.Entries[0].URL != "http://proxy-one:8080" {
|
|
t.Fatalf("unexpected normalized URL for first entry: %s", cfg.Entries[0].URL)
|
|
}
|
|
if len(cfg.Entries[0].Tags) != 3 {
|
|
t.Fatalf("expected merged tags in first entry, got %#v", cfg.Entries[0].Tags)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeProxiesConfigRejectsInvalidEntries(t *testing.T) {
|
|
_, err := NormalizeProxiesConfig(ProxiesConfig{
|
|
Entries: []ProxyEntryConfig{{URL: "ftp://proxy:21", Tags: []string{"default"}}},
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected invalid scheme error")
|
|
}
|
|
|
|
_, err = NormalizeProxiesConfig(ProxiesConfig{
|
|
Entries: []ProxyEntryConfig{{URL: "http://proxy:8080", Tags: []string{" "}}},
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected empty tags error")
|
|
}
|
|
|
|
_, err = NormalizeProxiesConfig(ProxiesConfig{
|
|
Global: "ftp://proxy:21",
|
|
Entries: []ProxyEntryConfig{{URL: "http://proxy:8080", Tags: []string{"default"}}},
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected invalid global proxy scheme error")
|
|
}
|
|
}
|
|
|
|
func TestResolveEffectiveProxyPolicy(t *testing.T) {
|
|
offPolicy := ResolveEffectiveProxyPolicy("", "")
|
|
if offPolicy.Mode != ProxyModeOff {
|
|
t.Fatalf("expected mode off, got %s", offPolicy.Mode)
|
|
}
|
|
if offPolicy.Tag != "" {
|
|
t.Fatalf("expected empty tag for off mode, got %q", offPolicy.Tag)
|
|
}
|
|
|
|
tagOnlyPolicy := ResolveEffectiveProxyPolicy("", "US")
|
|
if tagOnlyPolicy.Mode != ProxyModeTagPool || tagOnlyPolicy.Tag != "us" {
|
|
t.Fatalf("unexpected effective policy with tag override: %#v", tagOnlyPolicy)
|
|
}
|
|
|
|
globalPolicy := ResolveEffectiveProxyPolicy("http://proxy-global:8080", "eu")
|
|
if globalPolicy.Mode != ProxyModeTagPool || globalPolicy.Tag != "" {
|
|
t.Fatalf("expected global proxy to ignore engine tags, got %#v", globalPolicy)
|
|
}
|
|
}
|
|
|
|
func TestProxyRegistryRoundRobinAndFailureRecovery(t *testing.T) {
|
|
registry, err := NewProxyRegistry([]ProxyEntryConfig{
|
|
{URL: "http://proxy1:8080", Tags: []string{"default"}},
|
|
{URL: "http://proxy2:8080", Tags: []string{"default"}},
|
|
}, 2)
|
|
if err != nil {
|
|
t.Fatalf("new proxy registry: %v", err)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
if got := registry.NextByTag("default"); got != "http://proxy1:8080" {
|
|
t.Fatalf("expected first proxy1, got %s", got)
|
|
}
|
|
if got := registry.NextByTag("default"); got != "http://proxy2:8080" {
|
|
t.Fatalf("expected second proxy2, got %s", got)
|
|
}
|
|
|
|
registry.ReportFailure(ctx, "http://proxy1:8080")
|
|
registry.ReportFailure(ctx, "http://proxy1:8080")
|
|
if got := registry.NextByTag("default"); got != "http://proxy2:8080" {
|
|
t.Fatalf("expected proxy2 while proxy1 disabled, got %s", got)
|
|
}
|
|
|
|
// Exhaust pool; quarantine kicks in, no proxy served immediately.
|
|
registry.ReportFailure(ctx, "http://proxy2:8080")
|
|
registry.ReportFailure(ctx, "http://proxy2:8080")
|
|
if got := registry.NextByTag("default"); got != "" {
|
|
t.Fatalf("expected empty while pool is quarantined, got %s", got)
|
|
}
|
|
|
|
expireProxyTagQuarantine(t, registry, "default")
|
|
|
|
// After quarantine expiry, one probe proxy is re-enabled.
|
|
got := registry.NextByTag("default")
|
|
if got == "" {
|
|
t.Fatal("expected a probe proxy after quarantine expiry, got empty")
|
|
}
|
|
|
|
// A success on the probe clears the quarantine and re-enables the pool.
|
|
registry.ReportSuccess(ctx, got)
|
|
stats := registry.BuildStats()
|
|
if stats.UnhealthyCount != 1 {
|
|
t.Fatalf("expected one still-disabled proxy after single probe recovery, got unhealthy_count=%d", stats.UnhealthyCount)
|
|
}
|
|
if stats.HealthyCount != 1 {
|
|
t.Fatalf("expected one healthy proxy after probe success, got %d", stats.HealthyCount)
|
|
}
|
|
}
|
|
|
|
func TestMaskProxyURLRedactsCredentials(t *testing.T) {
|
|
if got := MaskProxyURL("http://user:pass@127.0.0.1:8080"); got != "http://127.0.0.1:8080" {
|
|
t.Fatalf("unexpected masked proxy value: %s", got)
|
|
}
|
|
}
|
|
|
|
func TestBrowserOptsLogFieldsRedactSecrets(t *testing.T) {
|
|
fields := browserOptsLogFields(BrowserOpts{
|
|
ProxyURL: "http://user:sentinel-password@proxy.example:8080",
|
|
CaptchaSolverApiKey: "captcha-secret",
|
|
CaptchaSolverEnabled: true,
|
|
})
|
|
rendered := fmt.Sprintf("%v", fields)
|
|
if strings.Contains(rendered, "sentinel-password") || strings.Contains(rendered, "captcha-secret") {
|
|
t.Fatalf("browser option log fields leaked secret: %s", rendered)
|
|
}
|
|
if fields["proxy"] != "http://proxy.example:8080" {
|
|
t.Fatalf("expected masked proxy field, got %#v", fields["proxy"])
|
|
}
|
|
if fields["captcha_solver_has_key"] != true {
|
|
t.Fatalf("expected captcha key presence boolean")
|
|
}
|
|
}
|
|
|
|
func TestProxyURLForBrowserLaunchStripsCredentials(t *testing.T) {
|
|
u, err := url.Parse("http://user:pass@127.0.0.1:18888")
|
|
if err != nil {
|
|
t.Fatalf("parse proxy URL: %v", err)
|
|
}
|
|
|
|
got := proxyURLForBrowserLaunch(u)
|
|
want := "http://127.0.0.1:18888"
|
|
if got != want {
|
|
t.Fatalf("expected %q, got %q", want, got)
|
|
}
|
|
}
|
|
|
|
func TestProxyURLForBrowserLaunchNormalizesSocks5h(t *testing.T) {
|
|
u, err := url.Parse("socks5h://test:test@127.0.0.1:19080")
|
|
if err != nil {
|
|
t.Fatalf("parse proxy URL: %v", err)
|
|
}
|
|
|
|
got := proxyURLForBrowserLaunch(u)
|
|
want := "socks5://127.0.0.1:19080"
|
|
if got != want {
|
|
t.Fatalf("expected %q, got %q", want, got)
|
|
}
|
|
}
|
|
|
|
func TestProxyStatsMaskCredentials(t *testing.T) {
|
|
registry, err := NewProxyRegistry([]ProxyEntryConfig{
|
|
{URL: "http://user:pass@proxy.example:8080", Tags: []string{"default"}},
|
|
}, 1)
|
|
if err != nil {
|
|
t.Fatalf("new proxy registry: %v", err)
|
|
}
|
|
|
|
stats := registry.BuildStats()
|
|
if len(stats.Entries) != 1 {
|
|
t.Fatalf("expected one proxy stats entry, got %d", len(stats.Entries))
|
|
}
|
|
if got := stats.Entries[0].Proxy; got != "http://proxy.example:8080" {
|
|
t.Fatalf("expected masked proxy in stats, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeProxyTag(t *testing.T) {
|
|
tag, err := NormalizeProxyTag(" US ")
|
|
if err != nil {
|
|
t.Fatalf("normalize proxy tag: %v", err)
|
|
}
|
|
if tag != "us" {
|
|
t.Fatalf("expected normalized tag us, got %q", tag)
|
|
}
|
|
|
|
if _, err := NormalizeProxyTag(" "); err == nil {
|
|
t.Fatal("expected empty proxy tag validation error")
|
|
}
|
|
}
|
|
|
|
func TestNormalizeProxyRequestOverride(t *testing.T) {
|
|
override, err := NormalizeProxyRequestOverride(" direct ")
|
|
if err != nil {
|
|
t.Fatalf("normalize direct proxy override: %v", err)
|
|
}
|
|
if override != ProxyOverrideDirect {
|
|
t.Fatalf("expected direct override, got %q", override)
|
|
}
|
|
|
|
override, err = NormalizeProxyRequestOverride(" US ")
|
|
if err != nil {
|
|
t.Fatalf("normalize tagged proxy override: %v", err)
|
|
}
|
|
if override != "us" {
|
|
t.Fatalf("expected normalized tag override us, got %q", override)
|
|
}
|
|
|
|
override, err = NormalizeProxyRequestOverride("")
|
|
if err != nil {
|
|
t.Fatalf("normalize empty override: %v", err)
|
|
}
|
|
if override != "" {
|
|
t.Fatalf("expected empty override, got %q", override)
|
|
}
|
|
}
|
|
|
|
func TestIsAuthenticatedSocksProxyURL(t *testing.T) {
|
|
if !IsAuthenticatedSocksProxyURL("socks5h://user:pass@127.0.0.1:1080") {
|
|
t.Fatal("expected authenticated socks proxy to be detected")
|
|
}
|
|
if IsAuthenticatedSocksProxyURL("socks5://127.0.0.1:1080") {
|
|
t.Fatal("expected plain socks proxy to remain browser-compatible")
|
|
}
|
|
if IsAuthenticatedSocksProxyURL("http://user:pass@127.0.0.1:8080") {
|
|
t.Fatal("expected HTTP auth proxy to remain browser-compatible")
|
|
}
|
|
}
|
|
|
|
func TestNewRawHTTPClientSocks5hUsesProxyDNS(t *testing.T) {
|
|
target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte("proxied"))
|
|
}))
|
|
defer target.Close()
|
|
|
|
targetAddr, err := net.ResolveTCPAddr("tcp", target.Listener.Addr().String())
|
|
if err != nil {
|
|
t.Fatalf("resolve target listener: %v", err)
|
|
}
|
|
|
|
const proxyOnlyHost = "proxy-target.invalid"
|
|
proxyAddr := startSOCKS5TestServer(t, proxyOnlyHost, targetAddr.IP)
|
|
|
|
directClient := &http.Client{Timeout: 500 * time.Millisecond}
|
|
targetURL := fmt.Sprintf("http://%s:%d/", proxyOnlyHost, targetAddr.Port)
|
|
if _, err := directClient.Get(targetURL); err == nil {
|
|
t.Fatal("expected direct request to fail without proxy DNS")
|
|
}
|
|
|
|
client, err := NewRawHTTPClient(Query{ProxyURL: "socks5h://" + proxyAddr})
|
|
if err != nil {
|
|
t.Fatalf("new raw http client: %v", err)
|
|
}
|
|
|
|
resp, err := client.Get(targetURL)
|
|
if err != nil {
|
|
t.Fatalf("expected proxied request to succeed, got %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
t.Fatalf("read proxied body: %v", err)
|
|
}
|
|
if string(body) != "proxied" {
|
|
t.Fatalf("unexpected proxied body: %q", string(body))
|
|
}
|
|
}
|
|
|
|
func TestNewRawHTTPClientDirectTLSUsesHTTP1(t *testing.T) {
|
|
server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = w.Write([]byte(r.Proto))
|
|
}))
|
|
server.EnableHTTP2 = true
|
|
server.StartTLS()
|
|
defer server.Close()
|
|
|
|
client, err := NewRawHTTPClient(Query{Insecure: true})
|
|
if err != nil {
|
|
t.Fatalf("new raw http client: %v", err)
|
|
}
|
|
|
|
resp, err := client.Get(server.URL)
|
|
if err != nil {
|
|
t.Fatalf("expected direct TLS request to succeed, got %v", err)
|
|
}
|
|
defer DrainAndCloseResponse(resp)
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
t.Fatalf("read response body: %v", err)
|
|
}
|
|
if string(body) != "HTTP/1.1" {
|
|
t.Fatalf("expected raw client to use HTTP/1.1, got %q", string(body))
|
|
}
|
|
}
|
|
|
|
func TestClassifyProxyNetworkError(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
err error
|
|
want error
|
|
}{
|
|
{name: "timeout", err: timeoutTestError{}, want: ErrTimeout},
|
|
{name: "connect", err: errors.New("proxyconnect tcp: connection refused"), want: ErrProxyConnect},
|
|
{name: "auth", err: errors.New("Proxy Authentication Required 407"), want: ErrProxyAuth},
|
|
{name: "parser", err: ErrParser, want: nil},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := classifyProxyNetworkError(tt.err)
|
|
if tt.want == nil {
|
|
if got != tt.err {
|
|
t.Fatalf("expected unchanged error, got %v", got)
|
|
}
|
|
return
|
|
}
|
|
if !errors.Is(got, tt.want) {
|
|
t.Fatalf("expected %v, got %v", tt.want, got)
|
|
}
|
|
if !errors.Is(got, tt.err) {
|
|
t.Fatalf("expected original error to be preserved, got %v", got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestClassifyMainDocumentStatus(t *testing.T) {
|
|
if !errors.Is(classifyMainDocumentStatus(http.StatusForbidden), ErrBlocked) {
|
|
t.Fatal("expected 403 to classify as ErrBlocked")
|
|
}
|
|
if !errors.Is(classifyMainDocumentStatus(http.StatusTooManyRequests), ErrRateLimited) {
|
|
t.Fatal("expected 429 to classify as ErrRateLimited")
|
|
}
|
|
if classifyMainDocumentStatus(http.StatusOK) != nil {
|
|
t.Fatal("expected 200 to remain unclassified")
|
|
}
|
|
}
|
|
|
|
func TestNewRawHTTPClientClassifiesProxyAuthFailure(t *testing.T) {
|
|
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusProxyAuthRequired)
|
|
}))
|
|
defer proxy.Close()
|
|
|
|
client, err := NewRawHTTPClient(Query{ProxyURL: proxy.URL})
|
|
if err != nil {
|
|
t.Fatalf("new raw http client: %v", err)
|
|
}
|
|
|
|
resp, err := client.Get("http://example.com/")
|
|
DrainAndCloseResponse(resp)
|
|
if !errors.Is(err, ErrProxyAuth) {
|
|
t.Fatalf("expected proxy auth error, got %v", err)
|
|
}
|
|
}
|
|
|
|
type staticResolver struct {
|
|
host string
|
|
ip net.IP
|
|
}
|
|
|
|
func (r staticResolver) Resolve(ctx xcontext.Context, name string) (xcontext.Context, net.IP, error) {
|
|
if name == r.host {
|
|
return ctx, r.ip, nil
|
|
}
|
|
return ctx, nil, net.UnknownNetworkError(name)
|
|
}
|
|
|
|
func expireProxyTagQuarantine(t *testing.T, registry *ProxyRegistry, tag string) {
|
|
t.Helper()
|
|
|
|
registry.mu.Lock()
|
|
defer registry.mu.Unlock()
|
|
registry.tagQuarantine[normalizeTag(tag)] = time.Now().Add(-time.Second)
|
|
}
|
|
|
|
func proxyHealthyCount(stats ProxyStats) int {
|
|
count := 0
|
|
for _, entry := range stats.Entries {
|
|
if !entry.Disabled {
|
|
count++
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
func TestReportFailureDoesNotDegradeProxyOnNonNetworkError(t *testing.T) {
|
|
registry, err := NewProxyRegistry([]ProxyEntryConfig{
|
|
{URL: "http://proxy1:8080", Tags: []string{"default"}},
|
|
}, 2)
|
|
if err != nil {
|
|
t.Fatalf("new proxy registry: %v", err)
|
|
}
|
|
|
|
rs := &ResilientSearcher{proxyRegistry: registry}
|
|
for _, err := range []error{ErrParser, ErrCaptcha} {
|
|
for range 50 {
|
|
rs.reportProxyAttempt(context.Background(), "http://proxy1:8080", err)
|
|
}
|
|
}
|
|
|
|
stats := registry.BuildStats()
|
|
if stats.UnhealthyCount != 0 {
|
|
t.Fatalf("non-network errors must not degrade proxy health: unhealthy_count=%d", stats.UnhealthyCount)
|
|
}
|
|
}
|
|
|
|
func TestReportFailureDegradeProxyOnNetworkError(t *testing.T) {
|
|
registry, err := NewProxyRegistry([]ProxyEntryConfig{
|
|
{URL: "http://proxy1:8080", Tags: []string{"default"}},
|
|
}, 2)
|
|
if err != nil {
|
|
t.Fatalf("new proxy registry: %v", err)
|
|
}
|
|
|
|
rs := &ResilientSearcher{proxyRegistry: registry}
|
|
rs.reportProxyAttempt(context.Background(), "http://proxy1:8080", ErrProxyConnect)
|
|
rs.reportProxyAttempt(context.Background(), "http://proxy1:8080", ErrProxyConnect)
|
|
|
|
stats := registry.BuildStats()
|
|
if stats.UnhealthyCount != 1 {
|
|
t.Fatalf("proxy network errors must degrade proxy: unhealthy_count=%d", stats.UnhealthyCount)
|
|
}
|
|
}
|
|
|
|
func TestPoolQuarantineAfterExhaustion(t *testing.T) {
|
|
registry, err := NewProxyRegistry([]ProxyEntryConfig{
|
|
{URL: "http://proxy1:8080", Tags: []string{"default"}},
|
|
}, 1)
|
|
if err != nil {
|
|
t.Fatalf("new proxy registry: %v", err)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
// Exhaust the single proxy; this should set a quarantine.
|
|
registry.ReportFailure(ctx, "http://proxy1:8080")
|
|
|
|
// Verify quarantine is set and NextByTag returns empty.
|
|
got := registry.NextByTagWithContext(ctx, "default")
|
|
if got != "" {
|
|
t.Fatalf("expected empty result during quarantine, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestPoolQuarantineExpiresAndProbesSingleProxy(t *testing.T) {
|
|
registry, err := NewProxyRegistry([]ProxyEntryConfig{
|
|
{URL: "http://proxy1:8080", Tags: []string{"default"}},
|
|
{URL: "http://proxy2:8080", Tags: []string{"default"}},
|
|
}, 1)
|
|
if err != nil {
|
|
t.Fatalf("new proxy registry: %v", err)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
// Exhaust both proxies.
|
|
registry.ReportFailure(ctx, "http://proxy1:8080")
|
|
registry.ReportFailure(ctx, "http://proxy2:8080")
|
|
|
|
expireProxyTagQuarantine(t, registry, "default")
|
|
|
|
// After expiry, exactly one proxy should be re-enabled as a probe.
|
|
got := registry.NextByTagWithContext(ctx, "default")
|
|
if got == "" {
|
|
t.Fatal("expected a proxy after quarantine expiry, got empty")
|
|
}
|
|
|
|
stats := registry.BuildStats()
|
|
if got := proxyHealthyCount(stats); got != 1 {
|
|
t.Fatalf("expected exactly 1 probe proxy re-enabled after quarantine, got %d healthy", got)
|
|
}
|
|
}
|
|
|
|
func TestPoolQuarantineRestartsAfterFailedProbe(t *testing.T) {
|
|
registry, err := NewProxyRegistry([]ProxyEntryConfig{
|
|
{URL: "http://proxy1:8080", Tags: []string{"default"}},
|
|
}, 1)
|
|
if err != nil {
|
|
t.Fatalf("new proxy registry: %v", err)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
registry.ReportFailure(ctx, "http://proxy1:8080")
|
|
expireProxyTagQuarantine(t, registry, "default")
|
|
|
|
probe := registry.NextByTagWithContext(ctx, "default")
|
|
if probe == "" {
|
|
t.Fatal("expected probe proxy after quarantine expiry")
|
|
}
|
|
|
|
registry.ReportFailure(ctx, probe)
|
|
if got := registry.NextByTagWithContext(ctx, "default"); got != "" {
|
|
t.Fatalf("expected renewed quarantine after failed probe, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestReportSuccessClearsQuarantine(t *testing.T) {
|
|
registry, err := NewProxyRegistry([]ProxyEntryConfig{
|
|
{URL: "http://proxy1:8080", Tags: []string{"default"}},
|
|
}, 1)
|
|
if err != nil {
|
|
t.Fatalf("new proxy registry: %v", err)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
registry.ReportFailure(ctx, "http://proxy1:8080")
|
|
|
|
// Confirm quarantine set.
|
|
if registry.NextByTagWithContext(ctx, "default") != "" {
|
|
t.Fatal("expected pool to be quarantined after exhaustion")
|
|
}
|
|
|
|
// Recovery: success clears quarantine.
|
|
registry.ReportSuccess(ctx, "http://proxy1:8080")
|
|
|
|
got := registry.NextByTagWithContext(ctx, "default")
|
|
if got == "" {
|
|
t.Fatal("expected proxy available after successful recovery")
|
|
}
|
|
}
|
|
|
|
func startSOCKS5TestServer(t *testing.T, host string, ip net.IP) string {
|
|
t.Helper()
|
|
|
|
server, err := socks5.New(&socks5.Config{
|
|
Resolver: staticResolver{host: host, ip: ip},
|
|
Logger: log.New(io.Discard, "", 0),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create socks5 server: %v", err)
|
|
}
|
|
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("listen socks5: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
_ = listener.Close()
|
|
})
|
|
|
|
go func() {
|
|
_ = server.Serve(listener)
|
|
}()
|
|
|
|
return listener.Addr().String()
|
|
}
|