由 Codex (gpt-5.6-sol) 实施,Claude 审核。 internal/huohanhan/auth.go - 六步登录:取 CID/CST 作 Basic 认证 → getCltConf 取 clientId → 下载验证码 → OCR 识别 → 提交登录 → app-version/info 在线验证 - OCR 响应递归查找 4~8 位验证码,优先 text/result/data/content/captcha/code - 错误分支:invalid_verify_code 换图重试(上限可配,默认 3); invalid_credentials 与 disabled_credentials 直接报中文错误不重试 - 认证复用改用 SQLite kv 表 + sync.Mutex,不照搬 Python 版的 Redis - GetValidAuth 按「内存 → SQLite 在线验证 → 重新登录」顺序取认证 internal/huohanhan/client.go - 统一请求方法,自动带 Authorization 与 cookies - 认证失败时清状态、重登、最多重试一次;超时、500、普通业务错误不重试 app.go - 新增 TestHuohanhanLogin,供设置页「测试连接」调用 测试全部使用 httptest 假服务,不需要真实账号即可运行。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LbdtsD3ohhSMy3KPoCgARq
169 lines
5.6 KiB
Go
169 lines
5.6 KiB
Go
package huohanhan
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
|
|
"cmsp/internal/logx"
|
|
)
|
|
|
|
func Test业务请求401后自动重登并只重试一次(t *testing.T) {
|
|
backend := newFakeLoginBackend(t)
|
|
logger := logx.New(100)
|
|
manager := newTestAuthManager(t, backend, newTestStore(t), logger, 3)
|
|
if _, err := manager.ForceLogin(context.Background()); err != nil {
|
|
t.Fatalf("准备初始认证失败:%v", err)
|
|
}
|
|
|
|
businessCalls := 0
|
|
backend.businessHandler = func(w http.ResponseWriter, r *http.Request) {
|
|
businessCalls++
|
|
if r.URL.Path != "/api/product/list" {
|
|
t.Errorf("业务路径期望 /api/product/list,实际 %s", r.URL.Path)
|
|
}
|
|
assertBusinessAuth(t, r)
|
|
if businessCalls == 1 {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
writeJSON(w, map[string]any{"ok": true})
|
|
}
|
|
|
|
client, err := NewClient(manager.cfg, manager, logger, backend.server.Client())
|
|
if err != nil {
|
|
t.Fatalf("创建客户端失败:%v", err)
|
|
}
|
|
response, err := client.Request(context.Background(), http.MethodPost, "product/list", []byte("page=1"), "application/x-www-form-urlencoded")
|
|
if err != nil {
|
|
t.Fatalf("401 后重登重试应成功,实际错误:%v", err)
|
|
}
|
|
defer response.Body.Close()
|
|
body, _ := io.ReadAll(response.Body)
|
|
if !strings.Contains(string(body), `"ok":true`) {
|
|
t.Fatalf("重试响应内容不正确:%s", body)
|
|
}
|
|
if businessCalls != 2 {
|
|
t.Fatalf("业务请求应执行 2 次,实际 %d 次", businessCalls)
|
|
}
|
|
if backend.loginCount != 2 {
|
|
t.Fatalf("初始登录加失效重登应共 2 次,实际 %d 次", backend.loginCount)
|
|
}
|
|
}
|
|
|
|
func Test业务请求连续401不会无限重试(t *testing.T) {
|
|
backend := newFakeLoginBackend(t)
|
|
manager := newTestAuthManager(t, backend, newTestStore(t), logx.New(100), 3)
|
|
if _, err := manager.ForceLogin(context.Background()); err != nil {
|
|
t.Fatalf("准备初始认证失败:%v", err)
|
|
}
|
|
|
|
businessCalls := 0
|
|
backend.businessHandler = func(w http.ResponseWriter, r *http.Request) {
|
|
businessCalls++
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
}
|
|
client, err := NewClient(manager.cfg, manager, logx.New(100), backend.server.Client())
|
|
if err != nil {
|
|
t.Fatalf("创建客户端失败:%v", err)
|
|
}
|
|
_, err = client.Request(context.Background(), http.MethodGet, "always-unauthorized", nil, "")
|
|
if err == nil || !strings.Contains(err.Error(), "重新登录后认证仍然失效") {
|
|
t.Fatalf("第二次 401 应停止并返回可读错误,实际 %v", err)
|
|
}
|
|
if businessCalls != 2 {
|
|
t.Fatalf("同一请求最多执行 2 次,实际 %d 次", businessCalls)
|
|
}
|
|
if backend.loginCount != 2 {
|
|
t.Fatalf("只应额外重登 1 次,实际总登录 %d 次", backend.loginCount)
|
|
}
|
|
}
|
|
|
|
func Test认证失败业务码触发一次重登(t *testing.T) {
|
|
backend := newFakeLoginBackend(t)
|
|
manager := newTestAuthManager(t, backend, newTestStore(t), logx.New(100), 3)
|
|
if _, err := manager.ForceLogin(context.Background()); err != nil {
|
|
t.Fatalf("准备初始认证失败:%v", err)
|
|
}
|
|
|
|
businessCalls := 0
|
|
backend.businessHandler = func(w http.ResponseWriter, r *http.Request) {
|
|
businessCalls++
|
|
if businessCalls == 1 {
|
|
writeJSON(w, map[string]any{"code": "invalid_token_expired"})
|
|
return
|
|
}
|
|
writeJSON(w, map[string]any{"data": "ok"})
|
|
}
|
|
client, err := NewClient(manager.cfg, manager, logx.New(100), backend.server.Client())
|
|
if err != nil {
|
|
t.Fatalf("创建客户端失败:%v", err)
|
|
}
|
|
response, err := client.Request(context.Background(), http.MethodGet, "auth-code", nil, "")
|
|
if err != nil {
|
|
t.Fatalf("认证失败业务码后应重试成功,实际错误:%v", err)
|
|
}
|
|
response.Body.Close()
|
|
if businessCalls != 2 || backend.loginCount != 2 {
|
|
t.Fatalf("应请求 2 次且总登录 2 次,实际请求 %d 次、登录 %d 次", businessCalls, backend.loginCount)
|
|
}
|
|
}
|
|
|
|
func TestHTTP500和普通业务错误不重试(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
statusCode int
|
|
body any
|
|
wantError bool
|
|
}{
|
|
{"HTTP 500", http.StatusInternalServerError, map[string]any{"message": "fake failure"}, true},
|
|
{"普通业务错误", http.StatusOK, map[string]any{"code": "product_not_found"}, false},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
backend := newFakeLoginBackend(t)
|
|
manager := newTestAuthManager(t, backend, newTestStore(t), logx.New(100), 3)
|
|
if _, err := manager.ForceLogin(context.Background()); err != nil {
|
|
t.Fatalf("准备初始认证失败:%v", err)
|
|
}
|
|
calls := 0
|
|
backend.businessHandler = func(w http.ResponseWriter, r *http.Request) {
|
|
calls++
|
|
w.WriteHeader(tc.statusCode)
|
|
_ = json.NewEncoder(w).Encode(tc.body)
|
|
}
|
|
client, err := NewClient(manager.cfg, manager, logx.New(100), backend.server.Client())
|
|
if err != nil {
|
|
t.Fatalf("创建客户端失败:%v", err)
|
|
}
|
|
response, requestErr := client.Request(context.Background(), http.MethodGet, "ordinary-error", nil, "")
|
|
if tc.wantError && requestErr == nil {
|
|
t.Fatalf("期望返回错误,实际成功")
|
|
}
|
|
if !tc.wantError && requestErr != nil {
|
|
t.Fatalf("普通业务响应应交给调用方处理,实际错误:%v", requestErr)
|
|
}
|
|
if response != nil {
|
|
response.Body.Close()
|
|
}
|
|
if calls != 1 {
|
|
t.Fatalf("非认证错误不能重试,实际请求 %d 次", calls)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func assertBusinessAuth(t *testing.T, r *http.Request) {
|
|
t.Helper()
|
|
if got := r.Header.Get("Authorization"); got != "Bearer "+testToken {
|
|
t.Errorf("Authorization 不正确,实际 %q", got)
|
|
}
|
|
cookie, err := r.Cookie("auth-session")
|
|
if err != nil || cookie.Value != testCookie {
|
|
t.Errorf("业务请求应携带登录 cookie,实际 cookie=%v err=%v", cookie, err)
|
|
}
|
|
}
|