Files
Cyrene_v2/plugins/channels/weixin_ilink/login_state_test.go
T
AskaEth 95f97f5df3
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
docs(comments): 注释中文化 · plugins/channels/weixin_ilink(20 文件,纯替换)
- 只翻注释/文档字符串:剥掉行尾注释后,每条「-」都有逐字节相同的「+」对应行(8 组,代码零改动)
- 保留协议/线上标识符原文:errcode、bot_token、context_token、getupdates、binded_redirect、PKCS#7、ECB 等
- 术语对齐既有译法:owner→主人、peer→对端、long poll→长轮询、backoff→退避、sentinel error→哨兵错误
- 已知既有欠债(未顺手修):login_reuse_test.go 在 HEAD 上就未 gofmt 干净;login_test.go 的 TestStartLogin_ScanStatusTransition 91 行 >80(HEAD 同为 91 行)
2026-10-04 12:29:05 +08:00

190 lines
6.1 KiB
Go

package weixin_ilink
import (
"bytes"
"context"
"io"
"net/http"
"sync"
"testing"
"time"
)
// scriptedTransport 不做网络 I/O 即可应答 weixin_ilink 的各个端点。它记录每次
// get_qrcode_status 轮询命中的主机,便于测试断言 IDC 重定向路由;并用固定二维码
// 应答 get_bot_qrcode,用 statusFn 返回 get_qrcode_status 的响应体(n 是从 1 开始的
// 轮询次数)。
type scriptedTransport struct {
qr []byte
statusFn func(n int) []byte
mu sync.Mutex
statusHosts []string
}
func (t *scriptedTransport) RoundTrip(req *http.Request) (*http.Response, error) {
var body []byte
switch req.URL.Path {
case "/ilink/bot/get_bot_qrcode":
body = t.qr
case "/ilink/bot/get_qrcode_status":
t.mu.Lock()
n := len(t.statusHosts) + 1
t.statusHosts = append(t.statusHosts, req.URL.Host)
t.mu.Unlock()
body = t.statusFn(n)
default:
return &http.Response{
StatusCode: http.StatusNotFound,
Body: io.NopCloser(bytes.NewReader(nil)),
Header: http.Header{},
Request: req,
}, nil
}
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewReader(body)),
Header: http.Header{"Content-Type": []string{"application/json"}},
Request: req,
}, nil
}
func (t *scriptedTransport) hosts() []string {
t.mu.Lock()
defer t.mu.Unlock()
return append([]string(nil), t.statusHosts...)
}
// newScriptedAdapter 构造一个接到脚本化传输层的 Adapter(无真实网络),其基础 URL
// 无法解析——由该传输层拦截。
func newScriptedAdapter(cfg Config, tr http.RoundTripper) (*Adapter, error) {
return New(cfg, &http.Client{Transport: tr}, nil)
}
const scriptedQR = `{"qrcode":"qr-r","qrcode_img_content":"https://img/r"}`
const scriptedBaseURL = "https://ilinkai.weixin.qq.com"
// TestStartLogin_ScanButRedirectRoutesPoll 验证带 redirect_host 的
// scaned_but_redirect 响应会把后续 get_qrcode_status 轮询切到
// https://<redirect_host> 这个 IDC 主机。
func TestStartLogin_ScanButRedirectRoutesPoll(t *testing.T) {
t.Parallel()
tr := &scriptedTransport{
qr: []byte(scriptedQR),
statusFn: func(n int) []byte {
if n == 1 {
return []byte(`{"status":"scaned_but_redirect","redirect_host":"idc.weixin.qq.com"}`)
}
return []byte(`{"status":"confirmed","bot_token":"tok-x","ilink_bot_id":"bot-x","ilink_user_id":"o-1","baseurl":"https://ilinkai.weixin.qq.com"}`)
},
}
ad, err := newScriptedAdapter(Config{
BaseURL: scriptedBaseURL, LoginInterval: time.Millisecond, LoginTimeout: 5 * time.Second,
}, tr)
if err != nil {
t.Fatalf("New: %v", err)
}
if _, err := ad.StartLogin(context.Background()); err != nil {
t.Fatalf("StartLogin: %v", err)
}
waitFor(t, func() bool {
s, _ := ad.LoginStatus(context.Background())
return s == LoginStatusConfirmed
})
if got := ad.Config()["bot_token"]; got != "tok-x" {
t.Fatalf("bot_token = %v, want tok-x", got)
}
hosts := tr.hosts()
if len(hosts) < 2 {
t.Fatalf("status polls = %d, want >= 2 (first on base, second on redirect host)", len(hosts))
}
if hosts[0] != "ilinkai.weixin.qq.com" {
t.Fatalf("first status poll host = %q, want ilinkai.weixin.qq.com", hosts[0])
}
if hosts[1] != "idc.weixin.qq.com" {
t.Fatalf("second status poll host = %q, want idc.weixin.qq.com (IDC redirect)", hosts[1])
}
}
// TestStartLogin_VerifyCodeBlocked 验证 verify_code_blocked 会把登录转为失败。
func TestStartLogin_VerifyCodeBlocked(t *testing.T) {
t.Parallel()
tr := &scriptedTransport{
qr: []byte(scriptedQR),
statusFn: func(int) []byte { return []byte(`{"status":"verify_code_blocked"}`) },
}
ad, err := newScriptedAdapter(Config{
BaseURL: scriptedBaseURL, LoginInterval: time.Millisecond, LoginTimeout: 2 * time.Second,
}, tr)
if err != nil {
t.Fatalf("New: %v", err)
}
if _, err := ad.StartLogin(context.Background()); err != nil {
t.Fatalf("StartLogin: %v", err)
}
waitFor(t, func() bool {
s, _ := ad.LoginStatus(context.Background())
return s == LoginStatusFailed
})
if ad.IsConnected() {
t.Fatal("IsConnected() = true after verify_code_blocked")
}
}
// TestStartLogin_BindedRedirect 验证 binded_redirect 被当作已绑定(LoginStatus 为
// confirmed)而非失败。
func TestStartLogin_BindedRedirect(t *testing.T) {
t.Parallel()
tr := &scriptedTransport{
qr: []byte(scriptedQR),
statusFn: func(int) []byte { return []byte(`{"status":"binded_redirect"}`) },
}
ad, err := newScriptedAdapter(Config{
BaseURL: scriptedBaseURL, LoginInterval: time.Millisecond, LoginTimeout: 2 * time.Second,
}, tr)
if err != nil {
t.Fatalf("New: %v", err)
}
if _, err := ad.StartLogin(context.Background()); err != nil {
t.Fatalf("StartLogin: %v", err)
}
waitFor(t, func() bool {
s, _ := ad.LoginStatus(context.Background())
return s == LoginStatusConfirmed
})
if _, detail := ad.LoginStatus(context.Background()); detail != loginBindedRedirectDetail {
t.Fatalf("binded_redirect detail = %q, want %q", detail, loginBindedRedirectDetail)
}
}
// TestStartLogin_NeedVerifyCode 验证 need_verifycode 会暴露一个专属的 LoginStatus
// 子状态并附提示详情,随后超时转为失败。
func TestStartLogin_NeedVerifyCode(t *testing.T) {
t.Parallel()
tr := &scriptedTransport{
qr: []byte(scriptedQR),
statusFn: func(int) []byte { return []byte(`{"status":"need_verifycode"}`) },
}
ad, err := newScriptedAdapter(Config{
BaseURL: scriptedBaseURL, LoginInterval: time.Millisecond, LoginTimeout: 40 * time.Millisecond,
}, tr)
if err != nil {
t.Fatalf("New: %v", err)
}
if _, err := ad.StartLogin(context.Background()); err != nil {
t.Fatalf("StartLogin: %v", err)
}
waitFor(t, func() bool {
s, _ := ad.LoginStatus(context.Background())
return s == LoginStatusNeedVerifyCode
})
if _, detail := ad.LoginStatus(context.Background()); detail != loginNeedVerifyCodeHint {
t.Fatalf("need_verifycode detail = %q, want %q", detail, loginNeedVerifyCodeHint)
}
// 在没有验证码回传路径的情况下,流程最终超时转为失败。
waitFor(t, func() bool {
s, _ := ad.LoginStatus(context.Background())
return s == LoginStatusFailed
})
}