fix(t236): retry rejected ERP captchas

This commit is contained in:
QiuSW
2026-07-29 11:54:06 +08:00
parent d7ab970f5a
commit cf83fb50e4
6 changed files with 237 additions and 23 deletions
@@ -259,6 +259,162 @@ func TestSessionManagerEnsureAuthenticatedUsesRecognizerOnce(t *testing.T) {
}
}
func TestSessionManagerEnsureAuthenticatedRetriesCaptchaRejectionSixTimes(t *testing.T) {
var captchaCalls, loginCalls, userCalls int
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case CaptchaPath:
captchaCalls++
w.Header().Set("Content-Type", "image/png")
_, _ = w.Write([]byte("captcha-image"))
case LoginPath:
loginCalls++
w.Header().Set("Content-Type", "application/json")
if loginCalls <= maxCaptchaRetries {
_, _ = w.Write([]byte(`{"status":false,"data":null,"msg":"图片验证码不正确"}`))
return
}
_, _ = w.Write([]byte(`{"status":true,"data":{"user":{"id":12,"username":"test-user"}}}`))
case UserPath:
userCalls++
_, _ = w.Write([]byte(`{"status":true,"data":{"id":12,"username":"test-user"}}`))
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
var events []string
recognizer := &fixedRecognizer{code: "1234"}
manager, err := NewSessionManager(SessionConfig{
BaseURL: server.URL,
Username: "test-user",
Password: "test-password",
Timeout: time.Second,
AllowInsecureHTTP: true,
CaptchaRecognizer: recognizer,
DiagnosticLogger: func(event string) {
events = append(events, event)
},
})
if err != nil {
t.Fatalf("NewSessionManager() error = %v", err)
}
if err := manager.EnsureAuthenticated(context.Background()); err != nil {
t.Fatalf("EnsureAuthenticated() error = %v", err)
}
if captchaCalls != maxCaptchaAttempts || recognizer.calls != maxCaptchaAttempts ||
loginCalls != maxCaptchaAttempts || userCalls != 1 {
t.Fatalf(
"captcha/OCR/login/user calls = %d/%d/%d/%d",
captchaCalls,
recognizer.calls,
loginCalls,
userCalls,
)
}
actual := strings.Join(events, "\n")
for _, expected := range []string{
"erp_login_attempt attempt=1 max_attempts=7 result=captcha_rejected",
"erp_login_attempt attempt=6 max_attempts=7 result=captcha_rejected",
"erp_login_attempt attempt=7 max_attempts=7 result=success",
} {
if !strings.Contains(actual, expected) {
t.Fatalf("diagnostic log missing %q: %s", expected, actual)
}
}
for _, secret := range []string{"test-user", "test-password"} {
if strings.Contains(actual, secret) {
t.Fatalf("diagnostic log leaked %q: %s", secret, actual)
}
}
}
func TestSessionManagerEnsureAuthenticatedStopsAfterCaptchaRetries(t *testing.T) {
var captchaCalls, loginCalls int
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case CaptchaPath:
captchaCalls++
w.Header().Set("Content-Type", "image/png")
_, _ = w.Write([]byte("captcha-image"))
case LoginPath:
loginCalls++
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"status":false,"data":null,"msg":"图片验证码不正确"}`))
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
recognizer := &fixedRecognizer{code: "1234"}
manager := testSessionManager(t, server.URL, "test-user", "test-password")
manager.recognizer = recognizer
err := manager.EnsureAuthenticated(context.Background())
if !errors.Is(err, domain.ErrFreightSourceLoginRejected) {
t.Fatalf("EnsureAuthenticated() error = %v", err)
}
if captchaCalls != maxCaptchaAttempts || recognizer.calls != maxCaptchaAttempts ||
loginCalls != maxCaptchaAttempts {
t.Fatalf(
"captcha/OCR/login calls = %d/%d/%d",
captchaCalls,
recognizer.calls,
loginCalls,
)
}
}
func TestSessionManagerEnsureAuthenticatedDoesNotRetryOtherLoginRejections(t *testing.T) {
var captchaCalls, loginCalls int
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case CaptchaPath:
captchaCalls++
w.Header().Set("Content-Type", "image/png")
_, _ = w.Write([]byte("captcha-image"))
case LoginPath:
loginCalls++
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"status":false,"data":null,"msg":"账号或密码错误"}`))
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
recognizer := &fixedRecognizer{code: "1234"}
manager := testSessionManager(t, server.URL, "test-user", "test-password")
manager.recognizer = recognizer
err := manager.EnsureAuthenticated(context.Background())
if !errors.Is(err, domain.ErrFreightSourceLoginRejected) {
t.Fatalf("EnsureAuthenticated() error = %v", err)
}
if captchaCalls != 1 || recognizer.calls != 1 || loginCalls != 1 {
t.Fatalf(
"captcha/OCR/login calls = %d/%d/%d",
captchaCalls,
recognizer.calls,
loginCalls,
)
}
}
func TestCaptchaRejectedMessageIsExact(t *testing.T) {
for _, testCase := range []struct {
message string
want bool
}{
{message: "图片验证码不正确", want: true},
{message: " 图片验证码不正确 ", want: true},
{message: "验证码不正确", want: false},
{message: "账号或密码错误", want: false},
{message: "", want: false},
} {
if actual := captchaRejectedMessage(testCase.message); actual != testCase.want {
t.Fatalf("captchaRejectedMessage(%q) = %t, want %t", testCase.message, actual, testCase.want)
}
}
}
func TestSessionManagerEnsureAuthenticatedRequiresRecognizer(t *testing.T) {
manager := testSessionManager(t, "https://erp.example.test", "test-user", "test-password")
err := manager.EnsureAuthenticated(context.Background())