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

202 lines
7.1 KiB
Go

package weixin_ilink
import (
"bytes"
"context"
"fmt"
"log/slog"
"strings"
"testing"
"time"
)
// TestStartLogin_ScanedExposesWaitingDetail 验证观测到的线上状态 "scaned" 会以
// 向后兼容的 waiting 状态加「请确认」详情呈现给面板,使前端不新增状态就能保留其
// 等待界面。
func TestStartLogin_ScanedExposesWaitingDetail(t *testing.T) {
t.Parallel()
srv := newJSONServer(t, nil, func(req recordedReq) handlerResult {
switch req.path {
case "/ilink/bot/get_bot_qrcode":
return handlerResult{body: jsonBody(map[string]any{
"qrcode": "qr-s1", "qrcode_img_content": "https://img/s1",
})}
case "/ilink/bot/get_qrcode_status":
return handlerResult{body: jsonBody(map[string]any{"status": "scaned"})}
}
return handlerResult{status: 404}
})
cfg := Config{BaseURL: srv.URL, LoginInterval: time.Millisecond, LoginTimeout: 5 * time.Second}
ad := newAdapter(t, cfg, srv)
if _, err := ad.StartLogin(context.Background()); err != nil {
t.Fatalf("StartLogin: %v", err)
}
waitFor(t, func() bool {
status, detail := ad.LoginStatus(context.Background())
return status == LoginStatusWaiting && detail == loginScannedDetail
})
}
// TestStartLogin_ScanedPastWindowReusesQR 验证重新取码保护:一旦轮询观测到 "scaned"
// (等待确认),StartLogin 即使在超过 qrReuseWindow 后也复用当前二维码,绝不发出第二次
// get_bot_qrcode——待处理的确认不会被丢弃。
func TestStartLogin_ScanedPastWindowReusesQR(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-scaned", "qrcode_img_content": "https://img/scaned",
})}
case "/ilink/bot/get_qrcode_status":
return handlerResult{body: jsonBody(map[string]any{"status": "scaned"})}
}
return handlerResult{status: 404}
})
cfg := Config{BaseURL: srv.URL, LoginInterval: time.Millisecond, LoginTimeout: 5 * time.Second}
ad := newAdapter(t, cfg, srv)
first, err := ad.StartLogin(context.Background())
if err != nil {
t.Fatalf("first StartLogin: %v", err)
}
// 等到轮询 goroutine 观测到 "scaned"(该会话处于确认中)。
waitFor(t, func() bool {
ad.loginMu.Lock()
defer ad.loginMu.Unlock()
return ad.loginSession != nil && ad.loginSession.currentPhase() == "scaned"
})
// 把 startedAt 推到远超复用窗口之前,使保住该二维码的只能是保护逻辑而非窗口本身。
// 此时一次刷新仍必须复用它。
ad.loginMu.Lock()
if ad.loginSession != nil {
ad.loginSession.startedAt = time.Now().Add(-(60*time.Second + time.Second))
}
ad.loginMu.Unlock()
second, err := ad.StartLogin(context.Background())
if err != nil {
t.Fatalf("second StartLogin: %v", err)
}
if first.Qrcode != second.Qrcode || first.Image != second.Image {
t.Fatalf("scaned session re-minted: first=%+v second=%+v (want identical reuse)", first, second)
}
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 (confirming session must not be re-minted)", got)
}
}
// TestStartLogin_WaitingPastWindowReMints 验证显式刷新场景仍然成立:仍在等待(尚未
// 被扫描)且已过复用窗口的二维码会用新的 get_bot_qrcode 重新生成,且被取消的旧轮询
// 绝不会用 "failed" 覆盖新会话的状态。
func TestStartLogin_WaitingPastWindowReMints(t *testing.T) {
t.Parallel()
rec := &reqRecorder{}
mintCalls := 0
srv := newJSONServer(t, rec, func(req recordedReq) handlerResult {
switch req.path {
case "/ilink/bot/get_bot_qrcode":
mintCalls++
return handlerResult{body: jsonBody(map[string]any{
"qrcode": fmt.Sprintf("qr-w%d", mintCalls),
"qrcode_img_content": fmt.Sprintf("https://img/w%d", mintCalls),
})}
case "/ilink/bot/get_qrcode_status":
return handlerResult{body: jsonBody(map[string]any{"status": "wait"})}
}
return handlerResult{status: 404}
})
cfg := Config{BaseURL: srv.URL, LoginInterval: time.Millisecond, LoginTimeout: 5 * time.Second}
ad := newAdapter(t, cfg, srv)
first, err := ad.StartLogin(context.Background())
if err != nil {
t.Fatalf("first StartLogin: %v", err)
}
waitFor(t, func() bool {
ad.loginMu.Lock()
defer ad.loginMu.Unlock()
return ad.loginSession != nil && ad.loginSession.currentPhase() == "wait"
})
ad.loginMu.Lock()
if ad.loginSession != nil {
ad.loginSession.startedAt = time.Now().Add(-(60*time.Second + time.Second))
}
ad.loginMu.Unlock()
second, err := ad.StartLogin(context.Background())
if err != nil {
t.Fatalf("second StartLogin: %v", err)
}
if second.Qrcode == first.Qrcode {
t.Fatalf("waiting session past reuse window reused stale QR %q, want a fresh code", first.Qrcode)
}
if got := len(rec.find(func(q recordedReq) bool { return q.path == "/ilink/bot/get_bot_qrcode" })); got != 2 {
t.Fatalf("get_bot_qrcode called %d times, want 2 (past-window waiting refresh re-mints)", got)
}
// 被取消的旧轮询不得覆盖刚生成出来的 waiting 状态。
if status, _ := ad.LoginStatus(context.Background()); status != LoginStatusWaiting {
t.Fatalf("LoginStatus after re-mint = %q, want waiting (superceded session must not set failed)", status)
}
}
// TestLogin_LogRedaction 验证扫码登录路径只记录已脱敏的标识:可渲染的二维码 URL 与
// bot id 会出现,但原始 qrcode 标识符、bot_token 以及主人的 user_id 绝不写入。
func TestLogin_LogRedaction(t *testing.T) {
t.Parallel()
var buf bytes.Buffer
statusCalls := 0
srv := newJSONServer(t, nil, func(req recordedReq) handlerResult {
switch req.path {
case "/ilink/bot/get_bot_qrcode":
return handlerResult{body: jsonBody(map[string]any{
"qrcode": "qr-redact", "qrcode_img_content": "https://img/redact",
})}
case "/ilink/bot/get_qrcode_status":
statusCalls++
if statusCalls == 1 {
return handlerResult{body: jsonBody(map[string]any{"status": "scaned"})}
}
return handlerResult{body: jsonBody(map[string]any{
"status": "confirmed", "bot_token": "tok-secret",
"ilink_bot_id": "bot-redact", "ilink_user_id": "o-redact",
"baseurl": "https://ilinkai.weixin.qq.com",
})}
}
return handlerResult{status: 404}
})
cfg := Config{BaseURL: srv.URL, LoginInterval: time.Millisecond, LoginTimeout: 5 * time.Second}
ad, err := New(cfg, srv.Client(), slog.New(slog.NewTextHandler(&buf, nil)))
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 { return ad.IsConnected() })
logs := buf.String()
for _, want := range []string{
"get_bot_qrcode ok",
"https://img/redact", // 已脱敏的可渲染 URL
"login confirmed",
"bot-redact", // bot id 非敏感
} {
if !strings.Contains(logs, want) {
t.Fatalf("logs missing %q:\n%s", want, logs)
}
}
for _, secret := range []string{"qr-redact", "tok-secret", "o-redact", "qrcode=qr-redact"} {
if strings.Contains(logs, secret) {
t.Fatalf("logs leaked secret %q:\n%s", secret, logs)
}
}
}