Files
sundynix-agentix/sundynix-gateway/internal/wechat/mp_test.go
T
Blizzard 07955ddf07 feat(auth): 微信扫码登录后端 —— 网页授权 + ticket 轮询
服务号「植趣 ZeeQ」已认证,走网页授权(snsapi_base,只拿 openid、用户无感),
不接管消息推送,副作用最小。

流程:PC 建 ticket → 二维码指向 /wx/mp?t= → 用户微信扫码 → 302 到微信授权页 →
回调 /api/v1/wx/mp/callback 用 code 换 openid → 找/建用户 → ticket 置 authorized →
PC 轮询 /wx/mp/poll 拿到 authorized → 签发 JWT。ticket 一次性消费防重放。

- 配置(appid/secret/base_url)后台可改,secret AES 加密入库,与微信支付同一套 secrets;
- ticket 存 Redis(短 TTL),无 Redis 时回退进程内内存(本地单实例可用,生产必须有 Redis);
- User 加 wechat_openid。**部分唯一索引**(WHERE openid <> '')而非普通唯一:
  存量邮箱用户该列是空串,普通唯一索引会让多个空串互撞、AutoMigrate 直接失败
  —— 与之前 NULL 余额同类的坑,这次提前避开。

单测覆盖:授权 URL 拼接(含 #wechat_redirect 锚点必须在末尾)、secret 加密往返、
建号/查号、空 openid 不误命中存量用户。微信 API 调用依赖公网回调,本地测不了,
留待部署后真机扫码。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 08:44:00 +08:00

54 lines
1.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package wechat
import (
"strings"
"testing"
)
func TestConfig_Enabled(t *testing.T) {
if (Config{AppID: "x"}).Enabled() {
t.Fatal("缺 secret 不该 enabled")
}
if !(Config{AppID: "x", AppSecret: "y"}).Enabled() {
t.Fatal("齐全应 enabled")
}
}
func TestAuthorizeURL(t *testing.T) {
c := Config{AppID: "wxAPP"}
u := c.AuthorizeURL("https://a.b/cb", "tkt123")
// 必备参数与微信要求的锚点
for _, want := range []string{
"open.weixin.qq.com/connect/oauth2/authorize",
"appid=wxAPP",
"scope=snsapi_base",
"state=tkt123",
"redirect_uri=https%3A%2F%2Fa.b%2Fcb", // 必须 URL 编码
"#wechat_redirect", // 缺了微信不跳转
} {
if !strings.Contains(u, want) {
t.Fatalf("授权 URL 缺 %q%s", want, u)
}
}
// 锚点必须在最后
if !strings.HasSuffix(u, "#wechat_redirect") {
t.Fatalf("#wechat_redirect 必须在末尾:%s", u)
}
}
// secret 加密往返:落库是密文,取出还原成明文。
func TestConfig_SecretRoundTrip(t *testing.T) {
c := Config{AppID: "x", AppSecret: "the-plain-secret"}
stored, err := c.EncryptedForStore()
if err != nil {
t.Fatal(err)
}
if stored.AppSecret == "the-plain-secret" {
t.Fatal("落库的 secret 不该是明文")
}
back := stored.DecryptFromStore()
if back.AppSecret != "the-plain-secret" {
t.Fatalf("还原失败:%q", back.AppSecret)
}
}