| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163 |
- // wechat 客户端单测:code 换 openid(httptest 打桩微信服务端)+ 扫码授权 URL 构造。
- //
- // 不连真微信:BaseURL 注入 httptest.Server,覆盖成功 / errcode!=0 / 网络故障 / 响应非法 JSON,
- // 以及 BuildQRConnectURL 的参数完整性(appid/redirect_uri/scope= snsapi_login/state + #wechat_redirect)。
- package wechat
- import (
- "context"
- "net/http"
- "net/http/httptest"
- "regexp"
- "strings"
- "testing"
- "time"
- )
- // newStub 起一个 httptest 微信服务替身,按 handler 返回预置响应。
- func newStub(t *testing.T, handler http.HandlerFunc) *HTTPClient {
- t.Helper()
- srv := httptest.NewServer(handler)
- t.Cleanup(srv.Close)
- return &HTTPClient{HTTP: &http.Client{Timeout: 2 * time.Second}, BaseURL: srv.URL}
- }
- // TestExchangeCode_Success 微信返回 openid:ExchangeCode 应取回原值。
- func TestExchangeCode_Success(t *testing.T) {
- var gotAppID, gotSecret, gotCode, gotRedirect string
- c := newStub(t, func(w http.ResponseWriter, r *http.Request) {
- q := r.URL.Query()
- gotAppID = q.Get("appid")
- gotSecret = q.Get("secret")
- gotCode = q.Get("code")
- gotRedirect = q.Get("redirect_uri")
- _, _ = w.Write([]byte(`{"access_token":"at-1","openid":"openid-x","errcode":0,"errmsg":"ok"}`))
- })
- oid, err := c.ExchangeCode(context.Background(), "appid-a", "secret-b", "code-1", "https://cb.test/auth/wechat/callback")
- if err != nil {
- t.Fatalf("ExchangeCode: %v", err)
- }
- if oid != "openid-x" {
- t.Fatalf("openid=%q(期望 openid-x)", oid)
- }
- if gotAppID != "appid-a" || gotSecret != "secret-b" || gotCode != "code-1" {
- t.Fatalf("请求参数不符:appid=%q secret=%q code=%q", gotAppID, gotSecret, gotCode)
- }
- if gotRedirect != "https://cb.test/auth/wechat/callback" {
- t.Fatalf("redirect_uri=%q(期望回调地址原样透传)", gotRedirect)
- }
- }
- // TestExchangeCode_WechatReject errcode!=0:返回 *errToken,IsTokenError 可判。
- func TestExchangeCode_WechatReject(t *testing.T) {
- c := newStub(t, func(w http.ResponseWriter, r *http.Request) {
- _, _ = w.Write([]byte(`{"errcode":40029,"errmsg":"invalid code"}`))
- })
- _, err := c.ExchangeCode(context.Background(), "a", "s", "bad", "")
- if err == nil {
- t.Fatal("errcode!=0 应返回 error")
- }
- if !IsTokenError(err) {
- t.Fatalf("应为 *errToken,实际 %T: %v", err, err)
- }
- }
- // TestExchangeCode_EmptyOpenID errcode=0 但 openid 缺失:按失败处理(防半成功态)。
- func TestExchangeCode_EmptyOpenID(t *testing.T) {
- c := newStub(t, func(w http.ResponseWriter, r *http.Request) {
- _, _ = w.Write([]byte(`{"access_token":"at","errcode":0,"errmsg":"ok"}`))
- })
- _, err := c.ExchangeCode(context.Background(), "a", "s", "code", "")
- if err == nil {
- t.Fatal("errcode=0 但 openid 空应返回 error(防半成功态)")
- }
- }
- // TestExchangeCode_NonJSON 响应非 JSON:返回 error(fail-closed,不误判成功)。
- func TestExchangeCode_NonJSON(t *testing.T) {
- c := newStub(t, func(w http.ResponseWriter, r *http.Request) {
- _, _ = w.Write([]byte("<html>bad gateway</html>"))
- })
- _, err := c.ExchangeCode(context.Background(), "a", "s", "code", "")
- if err == nil {
- t.Fatal("非 JSON 响应应返回 error")
- }
- }
- // TestExchangeCode_NetworkFail 网络故障(关 server):返回 *errToken。
- func TestExchangeCode_NetworkFail(t *testing.T) {
- srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
- c := &HTTPClient{HTTP: &http.Client{Timeout: 2 * time.Second}, BaseURL: srv.URL}
- srv.Close() // 关服务 → 连接失败
- _, err := c.ExchangeCode(context.Background(), "a", "s", "code", "")
- if err == nil {
- t.Fatal("网络故障应返回 error")
- }
- if !IsTokenError(err) {
- t.Fatalf("应为 *errToken,实际 %T", err)
- }
- }
- // TestBuildQRConnectURL 扫码授权 URL 参数完整 + 尾段 #wechat_redirect(微信约定必须保留)。
- func TestBuildQRConnectURL(t *testing.T) {
- u := BuildQRConnectURL("appid-a", "https://cb.test/auth/wechat/callback", "state-123")
- // 必含关键参数(redirect_uri 已 URL-encode,故用解码后断言)
- if !strings.Contains(u, "appid=appid-a") {
- t.Fatalf("缺 appid:%q", u)
- }
- if !strings.Contains(u, "response_type=code") {
- t.Fatalf("缺 response_type=code:%q", u)
- }
- if !strings.Contains(u, "scope=snsapi_login") {
- t.Fatalf("缺 scope=snsapi_login(网页扫码登录固定):%q", u)
- }
- if !strings.Contains(u, "state=state-123") {
- t.Fatalf("缺 state(防 CSRF):%q", u)
- }
- if !strings.HasSuffix(u, "#wechat_redirect") {
- t.Fatalf("缺 #wechat_redirect 尾段(微信约定必须保留):%q", u)
- }
- // redirect_uri 应出现(URL-encoded 形式)
- re := regexp.MustCompile(`redirect_uri=https%3A%2F%2Fcb\.test%2Fauth%2Fwechat%2Fcallback`)
- if !re.MatchString(u) {
- t.Fatalf("redirect_uri 未正确 URL-encode:%q", u)
- }
- }
- // TestBuildAuthorizeURL_Userinfo 移动 webview 授权 URL:oauth2/authorize + snsapi_userinfo + 无 #wechat_redirect。
- func TestBuildAuthorizeURL_Userinfo(t *testing.T) {
- u := BuildAuthorizeURL("appid-a", "https://cb.test/auth/wechat/callback", "state-456", ScopeUserinfo)
- if !strings.Contains(u, "https://open.weixin.qq.com/connect/oauth2/authorize?") {
- t.Fatalf("endpoint 应为 oauth2/authorize:%q", u)
- }
- if !strings.Contains(u, "appid=appid-a") {
- t.Fatalf("缺 appid:%q", u)
- }
- if !strings.Contains(u, "scope=snsapi_userinfo") {
- t.Fatalf("scope 应为 snsapi_userinfo:%q", u)
- }
- if !strings.Contains(u, "state=state-456") {
- t.Fatalf("缺 state(防 CSRF):%q", u)
- }
- if !strings.Contains(u, "response_type=code") {
- t.Fatalf("缺 response_type=code:%q", u)
- }
- // webview 场景无 #wechat_redirect 尾段
- if strings.Contains(u, "#wechat_redirect") {
- t.Fatalf("webview 场景不应含 #wechat_redirect:%q", u)
- }
- // redirect_uri 应出现(URL-encoded 形式)
- re := regexp.MustCompile(`redirect_uri=https%3A%2F%2Fcb\.test%2Fauth%2Fwechat%2Fcallback`)
- if !re.MatchString(u) {
- t.Fatalf("redirect_uri 未正确 URL-encode:%q", u)
- }
- }
- // TestBuildAuthorizeURL_Login PC 扫码走 BuildAuthorizeURL(ScopeLogin) 应与 BuildQRConnectURL 等价。
- func TestBuildAuthorizeURL_Login(t *testing.T) {
- u1 := BuildAuthorizeURL("appid-a", "https://cb.test/x", "s1", ScopeLogin)
- u2 := BuildQRConnectURL("appid-a", "https://cb.test/x", "s1")
- if u1 != u2 {
- t.Fatalf("BuildAuthorizeURL(ScopeLogin) 应等于 BuildQRConnectURL:\n A=%q\n B=%q", u1, u2)
- }
- }
|