2026-03-30 22:46:03 +03:00
|
|
|
//go:build integration
|
|
|
|
|
// +build integration
|
|
|
|
|
|
|
|
|
|
package core
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"io"
|
|
|
|
|
"net"
|
|
|
|
|
"net/http"
|
|
|
|
|
"net/url"
|
|
|
|
|
"os"
|
|
|
|
|
"strings"
|
|
|
|
|
"testing"
|
|
|
|
|
|
2026-04-01 00:54:21 +03:00
|
|
|
"github.com/karust/openserp/testutil"
|
2026-03-30 22:46:03 +03:00
|
|
|
"golang.org/x/time/rate"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const proxyIntegrationEnabledEnv = "OPENSERP_PROXY_TESTS"
|
|
|
|
|
|
|
|
|
|
type proxyIntegrationURLs struct {
|
|
|
|
|
targetURL string
|
|
|
|
|
socks5hAuthURL string
|
|
|
|
|
socks5hPlainURL string
|
|
|
|
|
httpAuthURL string
|
|
|
|
|
httpPlainURL string
|
|
|
|
|
badSocks5URL string
|
|
|
|
|
badHTTPURL string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestIntegrationSocks5hAuthProxyDNS(t *testing.T) {
|
|
|
|
|
cfg := proxyIntegrationConfig(t)
|
|
|
|
|
assertProxyFetchesTarget(t, cfg.targetURL, cfg.socks5hAuthURL)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestIntegrationSocks5hPlainProxyDNS(t *testing.T) {
|
|
|
|
|
cfg := proxyIntegrationConfig(t)
|
|
|
|
|
assertProxyFetchesTarget(t, cfg.targetURL, cfg.socks5hPlainURL)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestIntegrationHTTPAuthProxy(t *testing.T) {
|
|
|
|
|
cfg := proxyIntegrationConfig(t)
|
|
|
|
|
assertProxyFetchesTarget(t, cfg.targetURL, cfg.httpAuthURL)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestIntegrationHTTPPlainProxy(t *testing.T) {
|
|
|
|
|
cfg := proxyIntegrationConfig(t)
|
|
|
|
|
assertProxyFetchesTarget(t, cfg.targetURL, cfg.httpPlainURL)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestIntegrationRawSOCKSProxyPoolRotation(t *testing.T) {
|
|
|
|
|
cfg := proxyIntegrationConfig(t)
|
|
|
|
|
assertProxyPoolRotation(t, cfg.targetURL, []string{cfg.badSocks5URL, cfg.socks5hAuthURL})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestIntegrationRawHTTPProxyPoolRotation(t *testing.T) {
|
|
|
|
|
cfg := proxyIntegrationConfig(t)
|
|
|
|
|
assertProxyPoolRotation(t, cfg.targetURL, []string{cfg.badHTTPURL, cfg.httpAuthURL})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type proxyIntegrationEngine struct {
|
|
|
|
|
targetURL string
|
|
|
|
|
limiter *rate.Limiter
|
|
|
|
|
proxies []string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *proxyIntegrationEngine) Name() string {
|
|
|
|
|
return "google"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *proxyIntegrationEngine) IsInitialized() bool {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *proxyIntegrationEngine) GetRateLimiter() *rate.Limiter {
|
|
|
|
|
return e.limiter
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-20 05:53:13 +03:00
|
|
|
func (e *proxyIntegrationEngine) Search(_ context.Context, q Query) ([]SearchResult, error) {
|
2026-03-30 22:46:03 +03:00
|
|
|
e.proxies = append(e.proxies, q.ProxyURL)
|
|
|
|
|
|
|
|
|
|
body, err := fetchViaRawProxy(q.ProxyURL, q.Insecure, e.targetURL)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return []SearchResult{{
|
|
|
|
|
Rank: 1,
|
|
|
|
|
URL: e.targetURL,
|
|
|
|
|
Title: "proxy-ok",
|
|
|
|
|
Description: body,
|
|
|
|
|
}}, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-20 05:53:13 +03:00
|
|
|
func (e *proxyIntegrationEngine) SearchImage(_ context.Context, q Query) ([]SearchResult, error) {
|
|
|
|
|
_ = q
|
2026-03-30 22:46:03 +03:00
|
|
|
return nil, ErrSearchTimeout
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func proxyIntegrationConfig(t *testing.T) proxyIntegrationURLs {
|
|
|
|
|
t.Helper()
|
2026-04-01 00:54:21 +03:00
|
|
|
testutil.RequireIntegration(t)
|
2026-03-30 22:46:03 +03:00
|
|
|
|
|
|
|
|
if os.Getenv(proxyIntegrationEnabledEnv) != "1" {
|
|
|
|
|
t.Skipf("set %s=1 to run proxy integration tests", proxyIntegrationEnabledEnv)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return proxyIntegrationURLs{
|
|
|
|
|
targetURL: envOrDefault("OPENSERP_PROXY_TEST_TARGET_URL", "http://proxy-target:8080/"),
|
|
|
|
|
socks5hAuthURL: envOrDefault("OPENSERP_PROXY_TEST_SOCKS5H_AUTH_URL", "socks5h://test:test@127.0.0.1:19080"),
|
|
|
|
|
socks5hPlainURL: envOrDefault("OPENSERP_PROXY_TEST_SOCKS5H_PLAIN_URL", "socks5h://127.0.0.1:19082"),
|
|
|
|
|
httpAuthURL: envOrDefault("OPENSERP_PROXY_TEST_HTTP_AUTH_URL", "http://test:test@127.0.0.1:18888"),
|
|
|
|
|
httpPlainURL: envOrDefault("OPENSERP_PROXY_TEST_HTTP_PLAIN_URL", "http://127.0.0.1:18889"),
|
|
|
|
|
badSocks5URL: envOrDefault("OPENSERP_PROXY_TEST_BAD_SOCKS5_URL", "socks5://127.0.0.1:19081"),
|
|
|
|
|
badHTTPURL: envOrDefault("OPENSERP_PROXY_TEST_BAD_HTTP_URL", "http://127.0.0.1:18890"),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func assertProxyFetchesTarget(t *testing.T, targetURL, proxyURL string) {
|
|
|
|
|
t.Helper()
|
|
|
|
|
|
|
|
|
|
target := mustParseURL(t, targetURL)
|
|
|
|
|
assertHostCannotResolveTarget(t, target.Hostname())
|
|
|
|
|
|
|
|
|
|
body, err := fetchViaRawProxy(proxyURL, false, targetURL)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("expected proxied request via %s to succeed, got %v", proxyURL, err)
|
|
|
|
|
}
|
|
|
|
|
if !strings.Contains(body, "proxy-ok") {
|
|
|
|
|
t.Fatalf("expected proxy target response, got %q", body)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func assertProxyPoolRotation(t *testing.T, targetURL string, pool []string) {
|
|
|
|
|
t.Helper()
|
|
|
|
|
|
|
|
|
|
engine := &proxyIntegrationEngine{
|
|
|
|
|
targetURL: targetURL,
|
|
|
|
|
limiter: rate.NewLimiter(rate.Inf, 1),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
opts := DefaultServerOptions()
|
|
|
|
|
opts.Resilience.Retry.MaxRetries = 1
|
|
|
|
|
opts.Resilience.Retry.InitialBackoff = 0
|
|
|
|
|
opts.Resilience.Retry.MaxBackoff = 0
|
|
|
|
|
opts.Resilience.Retry.BackoffFactor = 1
|
|
|
|
|
opts.Resilience.Proxy = ProxyConfig{
|
2026-04-01 00:54:21 +03:00
|
|
|
Runtime: ProxyRuntimeRaw,
|
|
|
|
|
Proxies: ProxiesConfig{
|
|
|
|
|
Health: ProxiesHealthConfig{FailureThreshold: 1},
|
|
|
|
|
},
|
|
|
|
|
EnginePolicies: map[string]string{"google": "default"},
|
|
|
|
|
}
|
|
|
|
|
for _, proxyURL := range pool {
|
|
|
|
|
opts.Resilience.Proxy.Proxies.Entries = append(opts.Resilience.Proxy.Proxies.Entries, ProxyEntryConfig{
|
|
|
|
|
URL: proxyURL,
|
|
|
|
|
Tags: []string{"default"},
|
|
|
|
|
})
|
2026-03-30 22:46:03 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
srv := NewServerWithOptions("127.0.0.1", 7190, opts, engine)
|
|
|
|
|
resp := request(t, srv, "/google/search?text=proxy")
|
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
|
|
|
t.Fatalf("expected rotated proxy request to succeed, got %d", resp.StatusCode)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(engine.proxies) != 2 {
|
|
|
|
|
t.Fatalf("expected 2 proxy attempts, got %d", len(engine.proxies))
|
|
|
|
|
}
|
|
|
|
|
if engine.proxies[0] != pool[0] || engine.proxies[1] != pool[1] {
|
|
|
|
|
t.Fatalf("unexpected proxy rotation order: %#v", engine.proxies)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-01 00:54:21 +03:00
|
|
|
statsResp := request(t, srv, "/stats/proxy")
|
2026-03-30 22:46:03 +03:00
|
|
|
var stats map[string]interface{}
|
|
|
|
|
if err := json.NewDecoder(statsResp.Body).Decode(&stats); err != nil {
|
|
|
|
|
t.Fatalf("decode stats: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-01 00:54:21 +03:00
|
|
|
if got := stats["healthy_count"].(float64); got != 1 {
|
|
|
|
|
t.Fatalf("expected healthy_count=1, got %v", got)
|
2026-03-30 22:46:03 +03:00
|
|
|
}
|
2026-04-01 00:54:21 +03:00
|
|
|
if got := stats["unhealthy_count"].(float64); got != 1 {
|
|
|
|
|
t.Fatalf("expected unhealthy_count=1, got %v", got)
|
2026-03-30 22:46:03 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func fetchViaRawProxy(proxyURL string, insecure bool, targetURL string) (string, error) {
|
|
|
|
|
client, err := NewRawHTTPClient(Query{
|
|
|
|
|
ProxyURL: proxyURL,
|
|
|
|
|
Insecure: insecure,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resp, err := client.Get(targetURL)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
|
|
bodyBytes, err := io.ReadAll(resp.Body)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
return string(bodyBytes), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func assertHostCannotResolveTarget(t *testing.T, hostname string) {
|
|
|
|
|
t.Helper()
|
|
|
|
|
|
|
|
|
|
if _, err := net.DefaultResolver.LookupHost(context.Background(), hostname); err == nil {
|
|
|
|
|
t.Fatalf("expected direct host-side DNS lookup for %q to fail", hostname)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func mustParseURL(t *testing.T, raw string) *url.URL {
|
|
|
|
|
t.Helper()
|
|
|
|
|
|
|
|
|
|
parsed, err := url.Parse(raw)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("parse target URL %q: %v", raw, err)
|
|
|
|
|
}
|
|
|
|
|
return parsed
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func envOrDefault(key, fallback string) string {
|
|
|
|
|
if value := strings.TrimSpace(os.Getenv(key)); value != "" {
|
|
|
|
|
return value
|
|
|
|
|
}
|
|
|
|
|
return fallback
|
|
|
|
|
}
|