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") } }