- 只翻注释/文档字符串:剥掉行尾注释后,每条「-」都有逐字节相同的「+」对应行(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 行)
117 lines
4.1 KiB
Go
117 lines
4.1 KiB
Go
package weixin_ilink
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestStartLogin_WaitThenConfirm 驱动面板流程:StartLogin 返回二维码,后台轮询确认
|
|
// 绑定,LoginStatus 随之迁移。
|
|
func TestStartLogin_WaitThenConfirm(t *testing.T) {
|
|
t.Parallel()
|
|
rec := &reqRecorder{}
|
|
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": "data:image/png;base64,QUJD",
|
|
})}
|
|
case "/ilink/bot/get_qrcode_status":
|
|
return handlerResult{body: jsonBody(map[string]any{
|
|
"status": "confirmed", "bot_token": "tok-x",
|
|
"ilink_bot_id": "bot-x", "ilink_user_id": "o-1",
|
|
})}
|
|
}
|
|
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 != "qr-1" || qr.Image != "data:image/png;base64,QUJD" {
|
|
t.Fatalf("StartLogin returned %+v, want qr-1 + image", qr)
|
|
}
|
|
if status, _ := ad.LoginStatus(context.Background()); status != LoginStatusWaiting {
|
|
t.Fatalf("LoginStatus right after StartLogin = %q, want waiting", status)
|
|
}
|
|
|
|
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)
|
|
}
|
|
// 按官方协议,取码必须是携带 iLink 标识头的 POST。
|
|
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" || q.appClientVer != "132104" {
|
|
t.Fatalf("get_bot_qrcode iLink headers = (%q, %q), want (bot, 132104)", q.appID, q.appClientVer)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestStartLogin_IdempotentWhileWaiting 验证登录进行中时的第二次 StartLogin 返回同一
|
|
// 个二维码,且绝不发出第二次 get_bot_qrcode。
|
|
func TestStartLogin_IdempotentWhileWaiting(t *testing.T) {
|
|
t.Parallel()
|
|
rec := &reqRecorder{}
|
|
srv := newJSONServer(t, rec, func(req recordedReq) handlerResult {
|
|
if req.path == "/ilink/bot/get_bot_qrcode" {
|
|
return handlerResult{body: jsonBody(map[string]any{
|
|
"qrcode": "qr-2", "qrcode_img_content": "https://img/qr",
|
|
})}
|
|
}
|
|
return handlerResult{body: jsonBody(map[string]any{"status": "wait"})}
|
|
})
|
|
cfg := Config{BaseURL: srv.URL, LoginInterval: time.Hour, LoginTimeout: 5 * time.Second}
|
|
ad := newAdapter(t, cfg, srv)
|
|
|
|
first, err := ad.StartLogin(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("first StartLogin: %v", err)
|
|
}
|
|
second, err := ad.StartLogin(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("second StartLogin: %v", err)
|
|
}
|
|
if first.Qrcode != second.Qrcode {
|
|
t.Fatalf("second StartLogin qrcode = %q, want %q (idempotent)", second.Qrcode, first.Qrcode)
|
|
}
|
|
if got := len(rec.find(func(q recordedReq) bool { return q.path == "/ilink/bot/get_bot_qrcode" })); got != 1 {
|
|
t.Fatalf("get_bot_qrcode called %d times, want 1", got)
|
|
}
|
|
}
|
|
|
|
// TestStartLogin_AlreadyLoggedIn 验证已有 token 时 StartLogin 会拒绝,且 LoginStatus
|
|
// 报告 confirmed。
|
|
func TestStartLogin_AlreadyLoggedIn(t *testing.T) {
|
|
t.Parallel()
|
|
ad := newAdapter(t, Config{BaseURL: "https://example.invalid", BotToken: "tok-pre"}, nil)
|
|
_, err := ad.StartLogin(context.Background())
|
|
if !errors.Is(err, ErrAlreadyLoggedIn) {
|
|
t.Fatalf("StartLogin err = %v, want ErrAlreadyLoggedIn", err)
|
|
}
|
|
if status, detail := ad.LoginStatus(context.Background()); status != LoginStatusConfirmed || detail != "" {
|
|
t.Fatalf("LoginStatus = (%q, %q), want confirmed/empty", status, detail)
|
|
}
|
|
}
|
|
|
|
// TestLoginStatus_Idle 验证全新适配器报告 idle。
|
|
func TestLoginStatus_Idle(t *testing.T) {
|
|
t.Parallel()
|
|
ad := newAdapter(t, Config{BaseURL: "https://example.invalid"}, nil)
|
|
if status, _ := ad.LoginStatus(context.Background()); status != LoginStatusIdle {
|
|
t.Fatalf("LoginStatus = %q, want idle", status)
|
|
}
|
|
}
|