feat(channels/weixin_ilink): 微信 iLink 渠道插件(扫码登录+长轮询+游标+context_token+AES-128-ECB 媒体+24h 主动推送窗口)
This commit is contained in:
1 parent
7831b0ea1b
commit
88dc12466d
13 files changed
+2627
No files matched your search
@@ -0,0 +1,146 @@
|
||||
package weixin_ilink
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestConnect_ScanLogin_StatusTransition drives the QR status flow
|
||||
// wait → scaned → confirmed and asserts the credentials are stored.
|
||||
func TestConnect_ScanLogin_StatusTransition(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)
|
||||
if err := ad.Connect(context.Background()); err != nil {
|
||||
t.Fatalf("Connect: %v", err)
|
||||
}
|
||||
if !ad.IsConnected() {
|
||||
t.Fatal("IsConnected() = false after 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)
|
||||
}
|
||||
|
||||
reqs := rec.all()
|
||||
if len(reqs) < 3 {
|
||||
t.Fatalf("expected >=3 requests, got %d", len(reqs))
|
||||
}
|
||||
// Confirm the qrcode id is echoed in the status poll query.
|
||||
statusReqs := rec.find(func(q recordedReq) bool {
|
||||
return q.path == "/ilink/bot/get_qrcode_status"
|
||||
})
|
||||
if len(statusReqs) == 0 {
|
||||
t.Fatal("no get_qrcode_status requests recorded")
|
||||
}
|
||||
if len(statusReqs) > 0 {
|
||||
for i, q := range statusReqs {
|
||||
if q.query != "qrcode=qr-1" {
|
||||
t.Fatalf("status poll #%d query = %q, want qrcode=qr-1", i, q.query)
|
||||
}
|
||||
if q.authType != "ilink_bot_token" {
|
||||
t.Fatalf("status poll #%d AuthorizationType = %q, want ilink_bot_token", i, q.authType)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestConnect_ScanLogin_Timeout verifies the login aborts with ErrLoginTimeout
|
||||
// when the QR is never confirmed.
|
||||
func TestConnect_ScanLogin_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)
|
||||
err := ad.Connect(context.Background())
|
||||
if !errors.Is(err, ErrLoginTimeout) {
|
||||
t.Fatalf("err = %v, want ErrLoginTimeout", err)
|
||||
}
|
||||
if ad.IsConnected() {
|
||||
t.Fatal("IsConnected() = true after failed login")
|
||||
}
|
||||
}
|
||||
|
||||
// TestConnect_ScanLogin_EmptyQRCode verifies a qrcode-less response fails.
|
||||
func TestConnect_ScanLogin_EmptyQRCode(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": ""})}
|
||||
}
|
||||
return handlerResult{status: 404}
|
||||
})
|
||||
ad := newAdapter(t, Config{BaseURL: srv.URL}, srv)
|
||||
if err := ad.Connect(context.Background()); err == nil {
|
||||
t.Fatal("Connect() = nil, want error for empty qrcode")
|
||||
}
|
||||
}
|
||||
|
||||
// TestConnect_ScanLogin_ServerError verifies a non-2xx qrcode response fails.
|
||||
func TestConnect_ScanLogin_ServerError(t *testing.T) {
|
||||
t.Parallel()
|
||||
srv := newJSONServer(t, nil, func(req recordedReq) handlerResult {
|
||||
return handlerResult{status: 500, body: []byte("boom")}
|
||||
})
|
||||
ad := newAdapter(t, Config{BaseURL: srv.URL}, srv)
|
||||
if err := ad.Connect(context.Background()); err == nil {
|
||||
t.Fatal("Connect() = nil, want error on 500")
|
||||
}
|
||||
}
|
||||
|
||||
// TestConnect_WithToken_SkipsQR verifies Connect does not touch the network
|
||||
// when a bot token is already configured.
|
||||
func TestConnect_WithToken_SkipsQR(t *testing.T) {
|
||||
t.Parallel()
|
||||
rec := &reqRecorder{}
|
||||
srv := newJSONServer(t, rec, func(req recordedReq) handlerResult {
|
||||
return handlerResult{status: 500, body: []byte("should not be called")}
|
||||
})
|
||||
ad := newAdapter(t, Config{BaseURL: srv.URL, BotToken: "tok-pre"}, srv)
|
||||
if err := ad.Connect(context.Background()); err != nil {
|
||||
t.Fatalf("Connect: %v", err)
|
||||
}
|
||||
if got := rec.all(); len(got) != 0 {
|
||||
t.Fatalf("expected no network calls, got %d", len(got))
|
||||
}
|
||||
if !ad.IsConnected() {
|
||||
t.Fatal("IsConnected() = false with a pre-set token")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user