Files
Cyrene_v2/plugins/channels/weixin_ilink/login_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

147 lines
4.9 KiB
Go

package weixin_ilink
import (
"context"
"encoding/json"
"errors"
"net/http"
"testing"
"time"
)
// TestStartLogin_ScanStatusTransition 经带外 API 驱动二维码绑定流程:StartLogin
// 生成二维码,后台轮询走过 wait→scaned→confirmed 并保存凭据,LoginStatus 呈现
// confirmed。
func TestStartLogin_ScanStatusTransition(t *testing.T) {
t.Parallel()
rec := &reqRecorder{}
statusCalls := 0
srv := newJSONServer(t, rec, func(req recordedReq) handlerResult {
switch req.path {
case "/ilink/bot/get_bot_qrcode":
return handlerResult{body: jsonBody(map[string]any{
"qrcode": "qr-1", "qrcode_img_content": "https://img/qr",
})}
case "/ilink/bot/get_qrcode_status":
statusCalls++
switch statusCalls {
case 1:
return handlerResult{body: jsonBody(map[string]any{"status": "wait"})}
case 2:
return handlerResult{body: jsonBody(map[string]any{"status": "scaned"})}
default:
return handlerResult{body: jsonBody(map[string]any{
"status": "confirmed",
"bot_token": "tok-x",
"ilink_bot_id": "bot-x",
"ilink_user_id": "o-1",
"baseurl": "https://ilinkai.weixin.qq.com",
})}
}
}
return handlerResult{status: 404}
})
cfg := Config{BaseURL: srv.URL, LoginInterval: time.Millisecond, LoginTimeout: 5 * time.Second}
ad := newAdapter(t, cfg, srv)
qr, err := ad.StartLogin(context.Background())
if err != nil {
t.Fatalf("StartLogin: %v", err)
}
if qr.Qrcode == "" {
t.Fatal("StartLogin returned empty qrcode")
}
// 轮询 LoginStatus,直到后台流程确认。
deadline := time.Now().Add(3 * time.Second)
for time.Now().Before(deadline) {
status, _ := ad.LoginStatus(context.Background())
if status == LoginStatusConfirmed {
break
}
time.Sleep(5 * time.Millisecond)
}
if !ad.IsConnected() {
t.Fatal("IsConnected() = false after confirmed scan login")
}
if got := ad.Config()["bot_token"]; got != "tok-x" {
t.Fatalf("bot_token = %v, want tok-x", got)
}
if got := ad.Config()["bot_id"]; got != "bot-x" {
t.Fatalf("bot_id = %v, want bot-x", got)
}
// 确认 qrcode id 在状态轮询的查询串与认证头中被回传。
for _, q := range rec.find(func(q recordedReq) bool { return q.path == "/ilink/bot/get_qrcode_status" }) {
if q.query != "qrcode=qr-1" {
t.Fatalf("status poll query = %q, want qrcode=qr-1", q.query)
}
if q.authType != "ilink_bot_token" {
t.Fatalf("status poll AuthorizationType = %q, want ilink_bot_token", q.authType)
}
}
// 确认取码遵循官方协议:POST、iLink App 标识头,以及携带 local_token_list 的
// 请求体(全新绑定时为空)。
for _, q := range rec.find(func(q recordedReq) bool { return q.path == "/ilink/bot/get_bot_qrcode" }) {
if q.method != http.MethodPost {
t.Fatalf("get_bot_qrcode method = %q, want POST", q.method)
}
if q.appID != "bot" {
t.Fatalf("get_bot_qrcode iLink-App-Id = %q, want bot", q.appID)
}
if q.appClientVer != "132104" {
t.Fatalf("get_bot_qrcode iLink-App-ClientVersion = %q, want 132104", q.appClientVer)
}
if q.query != "bot_type=3" {
t.Fatalf("get_bot_qrcode query = %q, want bot_type=3", q.query)
}
ltl, ok := q.bodyJSON()["local_token_list"]
if !ok {
t.Fatalf("get_bot_qrcode body missing local_token_list: %s", q.body)
}
if ltl == nil {
t.Fatalf("get_bot_qrcode local_token_list = nil, want empty array []")
}
}
}
// TestStartLogin_Timeout 验证二维码始终未确认时绑定以 ErrLoginTimeout 终止,且空 token
// 的 Connect **不**阻塞(ErrNeedsLogin)。
func TestStartLogin_Timeout(t *testing.T) {
t.Parallel()
srv := newJSONServer(t, nil, func(req recordedReq) handlerResult {
if req.path == "/ilink/bot/get_bot_qrcode" {
return handlerResult{body: jsonBody(map[string]any{"qrcode": "qr-t"})}
}
return handlerResult{body: jsonBody(map[string]any{"status": "wait"})}
})
cfg := Config{BaseURL: srv.URL, LoginInterval: time.Millisecond, LoginTimeout: 25 * time.Millisecond}
ad := newAdapter(t, cfg, srv)
// 无 token 的 Connect 绝不能阻塞在二维码上——它上报 ErrNeedsLogin,让调用方把
// 主人引向带外的扫码登录面板。
if err := ad.Connect(context.Background()); !errors.Is(err, ErrNeedsLogin) {
t.Fatalf("Connect(empty token) = %v, want ErrNeedsLogin", err)
}
// 带外绑定若始终未确认,同样会超时。
if _, err := ad.StartLogin(context.Background()); err != nil {
t.Fatalf("StartLogin: %v", err)
}
deadline := time.Now().Add(2 * time.Second)
var lastStatus string
for time.Now().Before(deadline) {
lastStatus, _ = ad.LoginStatus(context.Background())
if lastStatus == LoginStatusFailed {
break
}
time.Sleep(5 * time.Millisecond)
}
if lastStatus != LoginStatusFailed {
t.Fatalf("LoginStatus = %q, want failed after timeout", lastStatus)
}
if ad.IsConnected() {
t.Fatal("IsConnected() = true after failed login")
}
}
var _ = json.RawMessage{} // 保留 encoding/json 的导入,供后续用例使用