mirror of
https://github.com/karust/openserp.git
synced 2026-08-05 16:53:54 +08:00
201 lines
5.4 KiB
Go
201 lines
5.4 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
}
|