package weixin_ilink import ( "context" "errors" "testing" "time" ) // TestStartLogin_WaitThenConfirm drives the panel flow: StartLogin returns the QR, // the background poll confirms the bind, and LoginStatus transitions. 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) } } // TestStartLogin_IdempotentWhileWaiting verifies a second StartLogin while a login // is in progress returns the same QR and never issues a second 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 verifies StartLogin refuses when a token exists and // LoginStatus reports 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 verifies a fresh adapter reports 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) } }