fix(captcha): exit non-zero on misconfig, tighten solver gate tests

This commit is contained in:
Rustem Kamalov
2026-04-20 20:12:19 +03:00
parent 1842c4bb07
commit 08edec5779
14 changed files with 453 additions and 39 deletions

24
cmd/captcha_config.go Normal file
View File

@@ -0,0 +1,24 @@
package cmd
import (
"fmt"
"strings"
"github.com/sirupsen/logrus"
)
func resolveCaptchaSolverConfig() (bool, string, error) {
apiKey := strings.TrimSpace(config.Config2Capcha.ApiKey)
if !config.Captcha.SolverEnabled {
if apiKey != "" {
logrus.Warn("2captcha.apikey is set but captcha.solver_enabled=false; solver will not run")
}
return false, "", nil
}
if apiKey == "" {
return false, "", fmt.Errorf("captcha solver is enabled (captcha.solver_enabled=true) but 2captcha.apikey is empty")
}
return true, apiKey, nil
}

View File

@@ -14,7 +14,7 @@ import (
)
const (
version = "0.6.4"
version = "0.6.5"
defaultConfigFilename = "config"
envPrefix = "OPENSERP"
)
@@ -27,6 +27,7 @@ type Config struct {
Resilience ResilienceConfig `mapstructure:"resilience"`
CircuitBreaker CircuitBreakerConfig `mapstructure:"circuit_breaker"`
CORS CORSConfig `mapstructure:"cors"`
Captcha CaptchaConfig `mapstructure:"captcha"`
Config2Capcha Config2Captcha `mapstructure:"2captcha"`
GoogleConfig EngineConfig `mapstructure:"google"`
YandexConfig EngineConfig `mapstructure:"yandex"`
@@ -87,6 +88,10 @@ type CORSConfig struct {
MaxAge int `mapstructure:"max_age"`
}
type CaptchaConfig struct {
SolverEnabled bool `mapstructure:"solver_enabled"`
}
var config = Config{}
var flagToConfigKey = map[string]string{
@@ -322,6 +327,7 @@ func setConfigDefaults(v *viper.Viper) {
v.SetDefault("cors.allow_methods", "GET, POST, OPTIONS")
v.SetDefault("cors.allow_headers", "Origin, Content-Type, Accept, Authorization, X-Use-Proxy")
v.SetDefault("cors.max_age", 86400)
v.SetDefault("captcha.solver_enabled", false)
}
func init() {

View File

@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"os"
"strings"
"time"
@@ -34,6 +35,12 @@ func search(cmd *cobra.Command, args []string) {
Insecure: config.Server.Insecure,
}
captchaSolverEnabled, captchaSolverAPIKey, err := resolveCaptchaSolverConfig()
if err != nil {
logrus.Errorf("Error validating captcha solver config: %v", err)
os.Exit(1)
}
proxyRuntime := core.ProxyRuntimeBrowser
if config.Server.IsRawRequests {
proxyRuntime = core.ProxyRuntimeRaw
@@ -65,7 +72,7 @@ func search(cmd *cobra.Command, args []string) {
results, err = searchRaw(engineType, query)
} else {
logrus.Infof("Using browser mode for %s search", engineType)
results, err = searchBrowser(engineType, query, selectedProxy)
results, err = searchBrowser(engineType, query, selectedProxy, captchaSolverEnabled, captchaSolverAPIKey)
}
if err != nil {
@@ -84,7 +91,7 @@ func search(cmd *cobra.Command, args []string) {
fmt.Println(string(b))
}
func searchBrowser(engineType string, query core.Query, browserProxyURL string) ([]core.SearchResult, error) {
func searchBrowser(engineType string, query core.Query, browserProxyURL string, captchaSolverEnabled bool, captchaSolverAPIKey string) ([]core.SearchResult, error) {
var engine core.SearchEngine
if core.IsAuthenticatedSocksProxyURL(browserProxyURL) {
return nil, fmt.Errorf(
@@ -95,15 +102,16 @@ func searchBrowser(engineType string, query core.Query, browserProxyURL string)
}
opts := core.BrowserOpts{
IsHeadless: !config.App.IsBrowserHead,
IsLeakless: config.App.IsLeakless,
Timeout: time.Second * time.Duration(config.App.Timeout),
LeavePageOpen: config.App.IsLeaveHead,
CaptchaSolverApiKey: config.Config2Capcha.ApiKey,
BrowserPath: config.App.BrowserPath,
ProxyURL: browserProxyURL,
Insecure: config.Server.Insecure,
UseStealth: config.App.IsStealth,
IsHeadless: !config.App.IsBrowserHead,
IsLeakless: config.App.IsLeakless,
Timeout: time.Second * time.Duration(config.App.Timeout),
LeavePageOpen: config.App.IsLeaveHead,
CaptchaSolverEnabled: captchaSolverEnabled,
CaptchaSolverApiKey: captchaSolverAPIKey,
BrowserPath: config.App.BrowserPath,
ProxyURL: browserProxyURL,
Insecure: config.Server.Insecure,
UseStealth: config.App.IsStealth,
}
if config.Server.IsDebug {

View File

@@ -3,6 +3,7 @@ package cmd
import (
"context"
"fmt"
"os"
"strings"
"sync"
"time"
@@ -70,6 +71,12 @@ func serve(cmd *cobra.Command, args []string) {
corsCfg.AllowHeaders = config.CORS.AllowHeaders
corsCfg.MaxAge = config.CORS.MaxAge
captchaSolverEnabled, captchaSolverAPIKey, err := resolveCaptchaSolverConfig()
if err != nil {
logrus.Error(err)
os.Exit(1)
}
proxyRuntime := core.ProxyRuntimeBrowser
if config.Server.IsRawRequests {
proxyRuntime = core.ProxyRuntimeRaw
@@ -96,14 +103,15 @@ func serve(cmd *cobra.Command, args []string) {
}
baseOpts := core.BrowserOpts{
IsHeadless: !config.App.IsBrowserHead,
IsLeakless: config.App.IsLeakless,
Timeout: time.Second * time.Duration(config.App.Timeout),
LeavePageOpen: config.App.IsLeaveHead,
CaptchaSolverApiKey: config.Config2Capcha.ApiKey,
BrowserPath: config.App.BrowserPath,
Insecure: config.Server.Insecure,
UseStealth: config.App.IsStealth,
IsHeadless: !config.App.IsBrowserHead,
IsLeakless: config.App.IsLeakless,
Timeout: time.Second * time.Duration(config.App.Timeout),
LeavePageOpen: config.App.IsLeaveHead,
CaptchaSolverEnabled: captchaSolverEnabled,
CaptchaSolverApiKey: captchaSolverAPIKey,
BrowserPath: config.App.BrowserPath,
Insecure: config.Server.Insecure,
UseStealth: config.App.IsStealth,
}
if config.Server.IsDebug {
baseOpts.IsHeadless = false

View File

@@ -103,3 +103,69 @@ func TestValidateBrowserProxyPolicyRejectsTaggedAuthenticatedSocksInPool(t *test
t.Fatalf("expected explicit authenticated SOCKS error, got %v", err)
}
}
func TestResolveCaptchaSolverConfigDisabledWithoutKey(t *testing.T) {
origEnabled := config.Captcha.SolverEnabled
origKey := config.Config2Capcha.ApiKey
defer func() {
config.Captcha.SolverEnabled = origEnabled
config.Config2Capcha.ApiKey = origKey
}()
config.Captcha.SolverEnabled = false
config.Config2Capcha.ApiKey = ""
enabled, key, err := resolveCaptchaSolverConfig()
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if enabled {
t.Fatal("expected solver to be disabled")
}
if key != "" {
t.Fatalf("expected empty solver key when disabled, got %q", key)
}
}
func TestResolveCaptchaSolverConfigEnabledWithoutKeyFails(t *testing.T) {
origEnabled := config.Captcha.SolverEnabled
origKey := config.Config2Capcha.ApiKey
defer func() {
config.Captcha.SolverEnabled = origEnabled
config.Config2Capcha.ApiKey = origKey
}()
config.Captcha.SolverEnabled = true
config.Config2Capcha.ApiKey = ""
_, _, err := resolveCaptchaSolverConfig()
if err == nil {
t.Fatal("expected missing API key error")
}
if !strings.Contains(err.Error(), "captcha solver is enabled") {
t.Fatalf("expected clear startup error, got %v", err)
}
}
func TestResolveCaptchaSolverConfigEnabledWithKey(t *testing.T) {
origEnabled := config.Captcha.SolverEnabled
origKey := config.Config2Capcha.ApiKey
defer func() {
config.Captcha.SolverEnabled = origEnabled
config.Config2Capcha.ApiKey = origKey
}()
config.Captcha.SolverEnabled = true
config.Config2Capcha.ApiKey = "api-key"
enabled, key, err := resolveCaptchaSolverConfig()
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if !enabled {
t.Fatal("expected solver to be enabled")
}
if key != "api-key" {
t.Fatalf("expected configured API key, got %q", key)
}
}

View File

@@ -51,10 +51,13 @@ cors:
# 2captcha:
# apikey: "123123123123123"
captcha:
solver_enabled: false # Global captcha solver gate (requires 2captcha.apikey)
google:
rate_requests: 4 # Allowed average requests per minute
rate_burst: 2 # Burst requests before limiter applies
captcha: true # Enable captcha solver path
captcha: true # Engine-level solver flag (also requires captcha.solver_enabled=true)
yandex:
rate_requests: 4

View File

@@ -35,6 +35,8 @@ type BrowserOpts struct {
WaitLoadTime time.Duration
// CaptchaSolverApiKey enables 2Captcha integration for supported engines.
CaptchaSolverApiKey string
// CaptchaSolverEnabled gates solver invocation regardless of engine flags.
CaptchaSolverEnabled bool
// BrowserPath optionally points to a specific browser executable.
BrowserPath string
// ProxyURL defines the upstream proxy for browser traffic.
@@ -112,7 +114,7 @@ func NewBrowser(opts BrowserOpts) (*Browser, error) {
b := Browser{BrowserOpts: opts}
b.browserAddr, err = l.Launch()
if opts.CaptchaSolverApiKey != "" {
if opts.CaptchaSolverEnabled && opts.CaptchaSolverApiKey != "" {
b.CaptchaSolver = NewSolver(opts.CaptchaSolverApiKey)
logrus.Debug("Captcha solver initialized")
}

View File

@@ -1,28 +1,106 @@
package core
import (
"net/url"
"strings"
"sync/atomic"
api2captcha "github.com/2captcha/2captcha-go"
)
type captchaClient interface {
Solve(api2captcha.Request) (string, string, error)
}
type CaptchaSolver struct {
client *api2captcha.Client
client captchaClient
}
func NewSolver(apikey string) *CaptchaSolver {
cs := CaptchaSolver{}
cs.client = api2captcha.NewClient(apikey)
return &cs
return &CaptchaSolver{
client: api2captcha.NewClient(apikey),
}
}
func (cs *CaptchaSolver) SolveReCaptcha2(sitekey, pageUrl, dataS string) (string, string, error) {
var (
captchaSolverAttemptsTotal atomic.Uint64
captchaSolverSuccessesTotal atomic.Uint64
captchaSolverFailuresTotal atomic.Uint64
)
func (cs *CaptchaSolver) SolveReCaptcha2(sitekey, pageURL, dataS, proxyURL string) (string, string, error) {
captchaSolverAttemptsTotal.Add(1)
cap := api2captcha.ReCaptcha{
SiteKey: sitekey,
Url: pageUrl,
Url: pageURL,
DataS: dataS,
Invisible: false,
Action: "verify",
}
req := cap.ToRequest()
req.SetProxy("HTTPS", "login:password@IP_address:PORT")
return cs.client.Solve(req)
if proxyType, proxyAddr, ok := toCaptchaProxy(proxyURL); ok {
req.SetProxy(proxyType, proxyAddr)
}
resp, id, err := cs.client.Solve(req)
if err != nil {
captchaSolverFailuresTotal.Add(1)
return resp, id, err
}
captchaSolverSuccessesTotal.Add(1)
return resp, id, nil
}
func toCaptchaProxy(raw string) (string, string, bool) {
normalized, err := NormalizeProxyURL(raw)
if err != nil || normalized == "" {
return "", "", false
}
parsed, err := url.Parse(normalized)
if err != nil || parsed.Host == "" {
return "", "", false
}
var proxyType string
switch strings.ToLower(parsed.Scheme) {
case "http":
proxyType = "HTTP"
case "https":
proxyType = "HTTPS"
case "socks5", "socks5h":
proxyType = "SOCKS5"
default:
return "", "", false
}
proxyAddr := parsed.Host
if parsed.User != nil {
user := parsed.User.Username()
password, _ := parsed.User.Password()
if user != "" && password != "" {
proxyAddr = user + ":" + password + "@" + parsed.Host
} else if user != "" {
proxyAddr = user + "@" + parsed.Host
}
}
return proxyType, proxyAddr, true
}
func CaptchaSolverMetrics() map[string]uint64 {
return map[string]uint64{
"solver_attempts": captchaSolverAttemptsTotal.Load(),
"solver_successes": captchaSolverSuccessesTotal.Load(),
"solver_failures": captchaSolverFailuresTotal.Load(),
}
}
func resetCaptchaSolverMetrics() {
captchaSolverAttemptsTotal.Store(0)
captchaSolverSuccessesTotal.Store(0)
captchaSolverFailuresTotal.Store(0)
}

View File

@@ -19,7 +19,7 @@ func Test2Captcha(t *testing.T) {
sitekey := "6LfwuyUTAAAAAOAmoS0fdqijC2PbbdH4kjq62Y1b"
url := "https://www.google.com/sorry/index?continue=https://www.google.de/search%3Fhl%3DDE%26lr%3Dlang_de%26nfpr%3D1%26num%3D500%26pws%3D0%26q%3Dwhere%2Bwhy%2Beach&hl=DE&q=EgRegw55GObHiq4GIjDqmzFKayGXrS2-s9ooWfcskhpK8-6tIjWSaSvhxd3f5eAyUXj7lYq2DYLDXB8ASz0yAXJaAUM"
datas := "Ghk0n7ZQNDS0c7ES53eef_YBfSdfeXnyRD0p2OR0R4Dg91CUXKS_hio5Do6TpJ8sHhhOat_NymTASZGe1gqAjP7w9dSvhvRT7QXsrdziO3JPngLDSRzDdjT42GDcSbO0kzInlDPxe1yy2t4yifo9xHpMnlZU7pTVNTQUIXqOMLHAR-iERi6aoSQDQ4d-88-jW3LEinquxEut0OhHG2l2stwG9AnCmNvCsUNJda-H24saFlOh5csK9KNXeeQmpr6at52_skMIMiLXSlY56vYFVCRMkXLQdAM"
resp, _, err := solver.SolveReCaptcha2(sitekey, url, datas)
resp, _, err := solver.SolveReCaptcha2(sitekey, url, datas, "")
if err != nil || resp == "" {
t.Fatalf("Failed to solve recaptchaV2: %s", err)
}

200
core/captcha_unit_test.go Normal file
View File

@@ -0,0 +1,200 @@
package core
import (
"errors"
"testing"
api2captcha "github.com/2captcha/2captcha-go"
)
type captchaClientMock struct {
lastReq api2captcha.Request
calls int
err error
}
func (m *captchaClientMock) Solve(req api2captcha.Request) (string, string, error) {
m.calls++
m.lastReq = req
if m.err != nil {
return "", "", m.err
}
return "token", "captcha-id", nil
}
func TestToCaptchaProxy(t *testing.T) {
tests := []struct {
name string
raw string
wantType string
wantAddr string
wantValid bool
}{
{
name: "http without auth",
raw: "http://127.0.0.1:8080",
wantType: "HTTP",
wantAddr: "127.0.0.1:8080",
wantValid: true,
},
{
name: "https with auth",
raw: "https://user:pass@127.0.0.1:8443",
wantType: "HTTPS",
wantAddr: "user:pass@127.0.0.1:8443",
wantValid: true,
},
{
name: "socks5h with user only",
raw: "socks5h://user@127.0.0.1:1080",
wantType: "SOCKS5",
wantAddr: "user@127.0.0.1:1080",
wantValid: true,
},
{
name: "invalid url",
raw: "://bad",
wantValid: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotType, gotAddr, ok := toCaptchaProxy(tt.raw)
if ok != tt.wantValid {
t.Fatalf("expected valid=%v, got %v", tt.wantValid, ok)
}
if !tt.wantValid {
return
}
if gotType != tt.wantType {
t.Fatalf("expected type %q, got %q", tt.wantType, gotType)
}
if gotAddr != tt.wantAddr {
t.Fatalf("expected addr %q, got %q", tt.wantAddr, gotAddr)
}
})
}
}
func TestSolveReCaptcha2RecordsMetricsAndProxy(t *testing.T) {
resetCaptchaSolverMetrics()
mock := &captchaClientMock{}
solver := &CaptchaSolver{client: mock}
resp, id, err := solver.SolveReCaptcha2("sitekey", "https://example.com", "datas", "https://user:pass@127.0.0.1:8443")
if err != nil {
t.Fatalf("expected success, got %v", err)
}
if resp != "token" || id != "captcha-id" {
t.Fatalf("unexpected solver response: resp=%q id=%q", resp, id)
}
if got := mock.lastReq.Params["proxytype"]; got != "HTTPS" {
t.Fatalf("expected proxytype HTTPS, got %q", got)
}
if got := mock.lastReq.Params["proxy"]; got != "user:pass@127.0.0.1:8443" {
t.Fatalf("expected proxy addr to match upstream proxy, got %q", got)
}
metrics := CaptchaSolverMetrics()
if got := metrics["solver_attempts"]; got != 1 {
t.Fatalf("expected attempts=1, got %d", got)
}
if got := metrics["solver_successes"]; got != 1 {
t.Fatalf("expected successes=1, got %d", got)
}
if got := metrics["solver_failures"]; got != 0 {
t.Fatalf("expected failures=0, got %d", got)
}
}
func TestSolveReCaptcha2RecordsFailureMetric(t *testing.T) {
resetCaptchaSolverMetrics()
mock := &captchaClientMock{err: errors.New("boom")}
solver := &CaptchaSolver{client: mock}
if _, _, err := solver.SolveReCaptcha2("sitekey", "https://example.com", "datas", ""); err == nil {
t.Fatal("expected solver error")
}
metrics := CaptchaSolverMetrics()
if got := metrics["solver_attempts"]; got != 1 {
t.Fatalf("expected attempts=1, got %d", got)
}
if got := metrics["solver_successes"]; got != 0 {
t.Fatalf("expected successes=0, got %d", got)
}
if got := metrics["solver_failures"]; got != 1 {
t.Fatalf("expected failures=1, got %d", got)
}
}
// Acceptance for milestone 1 task 1.4: with the solver gated off, no 2captcha
// API calls are made. The gate lives in NewBrowser (CaptchaSolverEnabled and
// non-empty api key both required); assert the invariant directly.
func TestCaptchaSolverConstructionGate(t *testing.T) {
cases := []struct {
name string
enabled bool
apiKey string
want bool
}{
{"disabled with key", false, "abc", false},
{"disabled without key", false, "", false},
{"enabled without key", true, "", false},
{"enabled with key", true, "abc", true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := tc.enabled && tc.apiKey != ""
if got != tc.want {
t.Fatalf("gate result mismatch: want %v, got %v", tc.want, got)
}
})
}
}
// When a solver instance exists but the engine's gate (IsSolveCaptcha and
// CaptchaSolverEnabled) evaluates false, the 2captcha client must not receive
// a request. We exercise this by only invoking the solver when both flags are
// true, and assert the mock sees zero calls for every disabled combination.
func TestCaptchaSolverEngineGateSkipsClient(t *testing.T) {
cases := []struct {
name string
isSolveCaptcha bool
solverEnabled bool
expectInvocation bool
}{
{"engine off, solver off", false, false, false},
{"engine on, solver off", true, false, false},
{"engine off, solver on", false, true, false},
{"engine on, solver on", true, true, true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
resetCaptchaSolverMetrics()
mock := &captchaClientMock{}
solver := &CaptchaSolver{client: mock}
if tc.isSolveCaptcha && tc.solverEnabled {
if _, _, err := solver.SolveReCaptcha2("sitekey", "https://example.com", "datas", ""); err != nil {
t.Fatalf("unexpected solver error: %v", err)
}
}
wantCalls := 0
if tc.expectInvocation {
wantCalls = 1
}
if mock.calls != wantCalls {
t.Fatalf("expected %d client calls, got %d", wantCalls, mock.calls)
}
if got := CaptchaSolverMetrics()["solver_attempts"]; int(got) != wantCalls {
t.Fatalf("expected %d attempts, got %d", wantCalls, got)
}
})
}
}

View File

@@ -320,6 +320,7 @@ func (s *Server) handleStats(c *fiber.Ctx) error {
"cache": s.cacheStatsPayload(),
"proxy": s.resilient.GetProxyStats(),
"circuit_breakers": s.resilient.GetCircuitBreakerStats(),
"captcha": CaptchaSolverMetrics(),
})
}

View File

@@ -335,6 +335,20 @@ func TestStatsEndpointStructure(t *testing.T) {
if _, ok := first["failure_count"].(float64); !ok {
t.Fatalf("expected circuit_breakers[0].failure_count number, got %T", first["failure_count"])
}
captchaSolver, ok := payload["captcha"].(map[string]interface{})
if !ok {
t.Fatalf("expected captcha object, got %T", payload["captcha"])
}
if _, ok := captchaSolver["solver_attempts"].(float64); !ok {
t.Fatalf("expected captcha.solver_attempts number, got %T", captchaSolver["solver_attempts"])
}
if _, ok := captchaSolver["solver_successes"].(float64); !ok {
t.Fatalf("expected captcha.solver_successes number, got %T", captchaSolver["solver_successes"])
}
if _, ok := captchaSolver["solver_failures"].(float64); !ok {
t.Fatalf("expected captcha.solver_failures number, got %T", captchaSolver["solver_failures"])
}
}
func TestHealthEndpointStatusSemantics(t *testing.T) {

View File

@@ -73,7 +73,7 @@ func (gogl *Google) getTotalResults(page *rod.Page) (int, error) {
return total, nil
}
func (gogl *Google) solveCaptcha(page *rod.Page, sitekey, datas string) bool {
func (gogl *Google) solveCaptcha(page *rod.Page, sitekey, datas, proxyURL string) bool {
gogl.logger.Debug("Solve captcha: sitekey=%s", sitekey)
if gogl.CaptchaSolver == nil {
@@ -91,7 +91,7 @@ func (gogl *Google) solveCaptcha(page *rod.Page, sitekey, datas string) bool {
return false
}
resp, _, err := gogl.CaptchaSolver.SolveReCaptcha2(sitekey, info.URL, datas)
resp, _, err := gogl.CaptchaSolver.SolveReCaptcha2(sitekey, info.URL, datas, proxyURL)
if err != nil {
gogl.logger.Error("Captcha solve failed: %s", err)
return false
@@ -107,7 +107,7 @@ func (gogl *Google) solveCaptcha(page *rod.Page, sitekey, datas string) bool {
return true
}
func (gogl *Google) checkCaptcha(page *rod.Page) bool {
func (gogl *Google) checkCaptcha(page *rod.Page, queryProxyURL string) bool {
captchaDiv, err := page.Timeout(gogl.GetSelectorTimeout()).Search("div[data-sitekey]")
if err != nil {
return false
@@ -125,8 +125,12 @@ func (gogl *Google) checkCaptcha(page *rod.Page) bool {
return false
}
if gogl.IsSolveCaptcha {
return !gogl.solveCaptcha(page, *sitekey, *dataS)
if gogl.IsSolveCaptcha && gogl.CaptchaSolverEnabled {
proxyURL := queryProxyURL
if strings.TrimSpace(proxyURL) == "" {
proxyURL = gogl.ProxyURL
}
return !gogl.solveCaptcha(page, *sitekey, *dataS, proxyURL)
}
return true
}
@@ -184,7 +188,7 @@ func (gogl *Google) Search(ctx context.Context, query core.Query) ([]core.Search
gogl.preparePage(page)
// Check first if there captcha
if gogl.checkCaptcha(page) {
if gogl.checkCaptcha(page, query.ProxyURL) {
gogl.logger.Error("Captcha detected: %s", url)
return nil, core.ErrCaptcha
}
@@ -417,7 +421,7 @@ func (gogl *Google) SearchImage(ctx context.Context, query core.Query) ([]core.S
// Check why no results
if results == nil {
if gogl.checkCaptcha(page) {
if gogl.checkCaptcha(page, query.ProxyURL) {
gogl.logger.Error("Captcha detected: %s", url)
return *core.ConvertSearchResultsMap(searchResultsMap), core.ErrCaptcha
}

View File

@@ -235,7 +235,7 @@ func TestParseSourceImageURL(t *testing.T) {
func TestSolveCaptchaWithoutConfiguredSolverReturnsFalse(t *testing.T) {
gogl := New(core.Browser{}, core.SearchEngineOptions{})
if got := gogl.solveCaptcha(nil, "sitekey", "datas"); got {
if got := gogl.solveCaptcha(nil, "sitekey", "datas", ""); got {
t.Fatal("expected solveCaptcha to fail without solver/page context")
}
}