|
@@ -120,11 +120,36 @@ var (
|
|
|
|
|
|
|
|
// ---- fixture ----
|
|
// ---- fixture ----
|
|
|
|
|
|
|
|
|
|
+// fakeWechat 微信客户端打桩:code→openid 映射(默认返回 "openid-fixed";可注入失败)。
|
|
|
|
|
+type fakeWechat struct {
|
|
|
|
|
+ byCode map[string]string
|
|
|
|
|
+ fail bool
|
|
|
|
|
+ called int
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (f *fakeWechat) ExchangeCode(_ context.Context, _, _, code, _ string) (string, error) {
|
|
|
|
|
+ f.called++
|
|
|
|
|
+ if f.fail {
|
|
|
|
|
+ return "", &wechatErr{}
|
|
|
|
|
+ }
|
|
|
|
|
+ if oid, ok := f.byCode[code]; ok {
|
|
|
|
|
+ return oid, nil
|
|
|
|
|
+ }
|
|
|
|
|
+ return "", &wechatErr{}
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// wechatErr 实现 wechat 包的"换 openid 失败"错误形态(IsTokenError 兼容:此处仅需非 nil error)。
|
|
|
|
|
+type wechatErr struct{}
|
|
|
|
|
+
|
|
|
|
|
+func (*wechatErr) Error() string { return "wechat: 换 openid 失败(测试打桩)" }
|
|
|
|
|
+
|
|
|
type fixture struct {
|
|
type fixture struct {
|
|
|
t *testing.T
|
|
t *testing.T
|
|
|
users *mockUsers
|
|
users *mockUsers
|
|
|
identities *mockIdentities
|
|
identities *mockIdentities
|
|
|
memberships *mockMemberships
|
|
memberships *mockMemberships
|
|
|
|
|
+ wechat *fakeWechat
|
|
|
|
|
+ wxSvc *service.WechatService
|
|
|
handler *Handler
|
|
handler *Handler
|
|
|
pubKeys []string
|
|
pubKeys []string
|
|
|
privPEM string
|
|
privPEM string
|
|
@@ -152,6 +177,7 @@ func newFixture(t *testing.T) *fixture {
|
|
|
refreshStore := appredis.NewRefreshStore(rc)
|
|
refreshStore := appredis.NewRefreshStore(rc)
|
|
|
revocation := appredis.NewRevocationStore(rc, "revoked")
|
|
revocation := appredis.NewRevocationStore(rc, "revoked")
|
|
|
tickets := appredis.NewLoginTicketStore(rc)
|
|
tickets := appredis.NewLoginTicketStore(rc)
|
|
|
|
|
+ wxStates := appredis.NewWechatQrcodeStore(rc)
|
|
|
|
|
|
|
|
issuer, err := token.NewIssuer(privPEM, "https://unified.test", 30*time.Minute, 30*24*time.Hour, refreshStore, revocation)
|
|
issuer, err := token.NewIssuer(privPEM, "https://unified.test", 30*time.Minute, 30*24*time.Hour, refreshStore, revocation)
|
|
|
if err != nil {
|
|
if err != nil {
|
|
@@ -169,12 +195,21 @@ func newFixture(t *testing.T) *fixture {
|
|
|
if err != nil {
|
|
if err != nil {
|
|
|
t.Fatalf("NewAuthService: %v", err)
|
|
t.Fatalf("NewAuthService: %v", err)
|
|
|
}
|
|
}
|
|
|
- h, err := New(Config{Svc: svc})
|
|
|
|
|
|
|
+ fw := &fakeWechat{byCode: map[string]string{}}
|
|
|
|
|
+ wxSvc, err := service.NewWechatService(service.WechatConfig{
|
|
|
|
|
+ Client: fw, States: wxStates, Tickets: tickets, Identities: ids,
|
|
|
|
|
+ Campuses: []service.WechatCampus{{CampusID: 3, AppID: "appid-test", Secret: "secret-test"}},
|
|
|
|
|
+ CallbackURL: "https://unified.test/auth/wechat/callback",
|
|
|
|
|
+ })
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("NewWechatService: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ h, err := New(Config{Svc: svc, Wechat: wxSvc})
|
|
|
if err != nil {
|
|
if err != nil {
|
|
|
t.Fatalf("New Handler: %v", err)
|
|
t.Fatalf("New Handler: %v", err)
|
|
|
}
|
|
}
|
|
|
return &fixture{
|
|
return &fixture{
|
|
|
- t: t, users: users, identities: ids, memberships: mergs, handler: h,
|
|
|
|
|
|
|
+ t: t, users: users, identities: ids, memberships: mergs, wechat: fw, wxSvc: wxSvc, handler: h,
|
|
|
pubKeys: pubKeys, privPEM: privPEM, issuer: issuer, revoke: revocation, refresh: refreshStore,
|
|
pubKeys: pubKeys, privPEM: privPEM, issuer: issuer, revoke: revocation, refresh: refreshStore,
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -195,6 +230,16 @@ func genRSAKey(t *testing.T) (string, []string) {
|
|
|
return privPEM, []string{pubPEM}
|
|
return privPEM, []string{pubPEM}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// addWechatUser 造一个用户 + wechat 身份(type=wechat, identifier=openid;扫码登录绑定用)。
|
|
|
|
|
+func (fx *fixture) addWechatUser(uid, openid string) {
|
|
|
|
|
+ fx.t.Helper()
|
|
|
|
|
+ if fx.users.byUID[uid] == nil {
|
|
|
|
|
+ fx.users.byUID[uid] = &model.User{CanonicalUserID: uid, PersonType: model.PersonTypeStaff, Status: model.UserStatusActive}
|
|
|
|
|
+ }
|
|
|
|
|
+ fx.identities.add(model.IdentityTypeWechat, openid,
|
|
|
|
|
+ &model.Identity{CanonicalUserID: uid, Type: model.IdentityTypeWechat, Identifier: openid, Verified: true})
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// addUser 造一个 argon2 用户 + username 身份(测试夹具快捷构造)。
|
|
// addUser 造一个 argon2 用户 + username 身份(测试夹具快捷构造)。
|
|
|
func (fx *fixture) addArgon2User(uid, username, password string) {
|
|
func (fx *fixture) addArgon2User(uid, username, password string) {
|
|
|
fx.t.Helper()
|
|
fx.t.Helper()
|
|
@@ -260,6 +305,7 @@ func (fx *fixture) newApp(claims *token.Claims) *fiber.App {
|
|
|
app.Post("/auth/refresh", fx.handler.Refresh)
|
|
app.Post("/auth/refresh", fx.handler.Refresh)
|
|
|
app.Get("/auth/wechat/qrcode", fx.handler.WechatQrcode)
|
|
app.Get("/auth/wechat/qrcode", fx.handler.WechatQrcode)
|
|
|
app.Get("/auth/wechat/status", fx.handler.WechatStatus)
|
|
app.Get("/auth/wechat/status", fx.handler.WechatStatus)
|
|
|
|
|
+ app.Get("/auth/wechat/callback", fx.handler.WechatCallback)
|
|
|
|
|
|
|
|
// 受保护路由(模拟 T4-5 中间件:把 claims 写入 c.Locals(ClaimsCtxKey))
|
|
// 受保护路由(模拟 T4-5 中间件:把 claims 写入 c.Locals(ClaimsCtxKey))
|
|
|
auth := app.Group("/auth")
|
|
auth := app.Group("/auth")
|
|
@@ -709,25 +755,253 @@ func TestLogout_NoClaims(t *testing.T) {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// ---- 微信占位 ----
|
|
|
|
|
|
|
+// ---- 微信扫码登录(状态机 + 复用两步票据)----
|
|
|
|
|
|
|
|
-// TestWechatQrcode_Placeholder 微信扫码占位:code 5001(暂未开放)。
|
|
|
|
|
-func TestWechatQrcode_Placeholder(t *testing.T) {
|
|
|
|
|
|
|
+// TestWechatQrcode_Success 生成二维码:code 200,data 含 state(32hex) + qrUrl(含 state) + campusId + expiresIn。
|
|
|
|
|
+func TestWechatQrcode_Success(t *testing.T) {
|
|
|
fx := newFixture(t)
|
|
fx := newFixture(t)
|
|
|
app := fx.newApp(nil)
|
|
app := fx.newApp(nil)
|
|
|
env := do(fx.t, app, http.MethodGet, "/auth/wechat/qrcode", "", nil)
|
|
env := do(fx.t, app, http.MethodGet, "/auth/wechat/qrcode", "", nil)
|
|
|
- if env.Code != apperrors.CodeInternal {
|
|
|
|
|
- t.Fatalf("code=%d(期望 %d 暂未开放)", env.Code, apperrors.CodeInternal)
|
|
|
|
|
|
|
+ if env.Code != 200 || !env.Success {
|
|
|
|
|
+ t.Fatalf("code=%d msg=%s(期望 200 成功)", env.Code, env.Message)
|
|
|
|
|
+ }
|
|
|
|
|
+ var qr struct {
|
|
|
|
|
+ State string `json:"state"`
|
|
|
|
|
+ QRURL string `json:"qrUrl"`
|
|
|
|
|
+ CampusID int64 `json:"campusId"`
|
|
|
|
|
+ ExpiresIn int64 `json:"expiresIn"`
|
|
|
|
|
+ }
|
|
|
|
|
+ if err := json.Unmarshal(env.Data, &qr); err != nil {
|
|
|
|
|
+ t.Fatalf("解析 data 失败: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if len(qr.State) != 32 {
|
|
|
|
|
+ t.Fatalf("state 长度=%d(期望 32 hex)", len(qr.State))
|
|
|
|
|
+ }
|
|
|
|
|
+ if !strings.Contains(qr.QRURL, "open.weixin.qq.com/connect/qrconnect") {
|
|
|
|
|
+ t.Fatalf("qrUrl=%q(期望微信网页授权 URL)", qr.QRURL)
|
|
|
|
|
+ }
|
|
|
|
|
+ if !strings.Contains(qr.QRURL, "state="+qr.State) {
|
|
|
|
|
+ t.Fatalf("qrUrl=%q(期望携带 state=%q)", qr.QRURL, qr.State)
|
|
|
|
|
+ }
|
|
|
|
|
+ if qr.CampusID != 3 {
|
|
|
|
|
+ t.Fatalf("campusId=%d(期望 3,配置首个 campus)", qr.CampusID)
|
|
|
|
|
+ }
|
|
|
|
|
+ if qr.ExpiresIn <= 0 {
|
|
|
|
|
+ t.Fatalf("expiresIn=%d(期望 >0 秒)", qr.ExpiresIn)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestWechatStatus_Pending 新 state 轮询:status=pending(继续轮询)。
|
|
|
|
|
+func TestWechatStatus_Pending(t *testing.T) {
|
|
|
|
|
+ fx := newFixture(t)
|
|
|
|
|
+ app := fx.newApp(nil)
|
|
|
|
|
+ qr := do(fx.t, app, http.MethodGet, "/auth/wechat/qrcode", "", nil)
|
|
|
|
|
+ var q struct {
|
|
|
|
|
+ State string `json:"state"`
|
|
|
|
|
+ }
|
|
|
|
|
+ _ = json.Unmarshal(qr.Data, &q)
|
|
|
|
|
+ env := do(fx.t, app, http.MethodGet, "/auth/wechat/status?state="+q.State, "", nil)
|
|
|
|
|
+ if env.Code != 200 {
|
|
|
|
|
+ t.Fatalf("code=%d msg=%s(期望 200)", env.Code, env.Message)
|
|
|
|
|
+ }
|
|
|
|
|
+ var st struct {
|
|
|
|
|
+ Status string `json:"status"`
|
|
|
|
|
+ }
|
|
|
|
|
+ if err := json.Unmarshal(env.Data, &st); err != nil {
|
|
|
|
|
+ t.Fatalf("解析 data 失败: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if st.Status != "pending" {
|
|
|
|
|
+ t.Fatalf("status=%q(期望 pending)", st.Status)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestWechatStatus_Expired 未知 state 轮询:expired=true(前端停轮询,提示刷新)。
|
|
|
|
|
+func TestWechatStatus_Expired(t *testing.T) {
|
|
|
|
|
+ fx := newFixture(t)
|
|
|
|
|
+ app := fx.newApp(nil)
|
|
|
|
|
+ env := do(fx.t, app, http.MethodGet, "/auth/wechat/status?state=doesnotexist00000000000000000000", "", nil)
|
|
|
|
|
+ if env.Code != 200 {
|
|
|
|
|
+ t.Fatalf("code=%d msg=%s(期望 200,expired 语义非故障)", env.Code, env.Message)
|
|
|
|
|
+ }
|
|
|
|
|
+ var st struct {
|
|
|
|
|
+ Expired bool `json:"expired"`
|
|
|
|
|
+ }
|
|
|
|
|
+ if err := json.Unmarshal(env.Data, &st); err != nil {
|
|
|
|
|
+ t.Fatalf("解析 data 失败: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if !st.Expired {
|
|
|
|
|
+ t.Fatal("未知 state 应 expired=true")
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestWechatCallback_Success 扫码成功(已绑定):code 换 openid → 发 loginTicket → status=success。
|
|
|
|
|
+// 校验:callback 返回 success + loginTicket + userId;state 内票据可被 ② issue 消费(复用两步②)。
|
|
|
|
|
+func TestWechatCallback_Success(t *testing.T) {
|
|
|
|
|
+ fx := newFixture(t)
|
|
|
|
|
+ uid := "01GAQF0Z6C1V9Y3E5H7K2M8B4T"
|
|
|
|
|
+ fx.addWechatUser(uid, "openid-bound")
|
|
|
|
|
+ fx.memberships.add(uid, orgmodel.Membership{
|
|
|
|
|
+ MembershipID: 1, CanonicalUserID: uid, InstitutionID: 11, CampusID: 3,
|
|
|
|
|
+ Role: "teacher", Status: "active",
|
|
|
|
|
+ })
|
|
|
|
|
+ fx.wechat.byCode["code-ok"] = "openid-bound"
|
|
|
|
|
+
|
|
|
|
|
+ app := fx.newApp(nil)
|
|
|
|
|
+ // ① 生成二维码
|
|
|
|
|
+ qrEnv := do(fx.t, app, http.MethodGet, "/auth/wechat/qrcode", "", nil)
|
|
|
|
|
+ var qr struct {
|
|
|
|
|
+ State string `json:"state"`
|
|
|
|
|
+ }
|
|
|
|
|
+ _ = json.Unmarshal(qrEnv.Data, &qr)
|
|
|
|
|
+
|
|
|
|
|
+ // ② 微信回调:code=code-ok, state=<state>
|
|
|
|
|
+ cbEnv := do(fx.t, app, http.MethodGet, "/auth/wechat/callback?code=code-ok&state="+qr.State, "", nil)
|
|
|
|
|
+ if cbEnv.Code != 200 {
|
|
|
|
|
+ t.Fatalf("callback code=%d msg=%s(期望 200 成功)", cbEnv.Code, cbEnv.Message)
|
|
|
|
|
+ }
|
|
|
|
|
+ var cb struct {
|
|
|
|
|
+ Status string `json:"status"`
|
|
|
|
|
+ LoginTicket string `json:"loginTicket"`
|
|
|
|
|
+ UserID string `json:"userId"`
|
|
|
|
|
+ }
|
|
|
|
|
+ if err := json.Unmarshal(cbEnv.Data, &cb); err != nil {
|
|
|
|
|
+ t.Fatalf("解析 callback data 失败: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if cb.Status != "success" {
|
|
|
|
|
+ t.Fatalf("status=%q(期望 success)", cb.Status)
|
|
|
|
|
+ }
|
|
|
|
|
+ if cb.UserID != uid {
|
|
|
|
|
+ t.Fatalf("userId=%q(期望 %q)", cb.UserID, uid)
|
|
|
|
|
+ }
|
|
|
|
|
+ if cb.LoginTicket == "" || len(cb.LoginTicket) != 32 {
|
|
|
|
|
+ t.Fatalf("loginTicket=%q(期望 32 hex)", cb.LoginTicket)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ③ 轮询确认 state=success 且携带同一 loginTicket
|
|
|
|
|
+ stEnv := do(fx.t, app, http.MethodGet, "/auth/wechat/status?state="+qr.State, "", nil)
|
|
|
|
|
+ var st struct {
|
|
|
|
|
+ Status string `json:"status"`
|
|
|
|
|
+ LoginTicket string `json:"loginTicket"`
|
|
|
|
|
+ }
|
|
|
|
|
+ _ = json.Unmarshal(stEnv.Data, &st)
|
|
|
|
|
+ if st.Status != "success" || st.LoginTicket != cb.LoginTicket {
|
|
|
|
|
+ t.Fatalf("轮询 status=%q ticket=%q(期望 success + 同票据)", st.Status, st.LoginTicket)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ④ 复用两步②:携 loginTicket 签 token(证明微信登录与账密登录走同一套 ②)
|
|
|
|
|
+ issueEnv := do(fx.t, app, http.MethodPost, "/auth/issue",
|
|
|
|
|
+ `{"loginTicket":"`+cb.LoginTicket+`","institutionId":11,"campusId":3}`, nil)
|
|
|
|
|
+ if issueEnv.Code != 200 {
|
|
|
|
|
+ t.Fatalf("issue code=%d msg=%s(期望 200:微信票据应可走同一套 ②)", issueEnv.Code, issueEnv.Message)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestWechatCallback_Unbound 扫码成功但 openid 未绑定:status=unbound(前端提示"未绑定账号")。
|
|
|
|
|
+func TestWechatCallback_Unbound(t *testing.T) {
|
|
|
|
|
+ fx := newFixture(t)
|
|
|
|
|
+ fx.wechat.byCode["code-x"] = "openid-not-bound" // 不造该 openid 的身份记录
|
|
|
|
|
+
|
|
|
|
|
+ app := fx.newApp(nil)
|
|
|
|
|
+ qrEnv := do(fx.t, app, http.MethodGet, "/auth/wechat/qrcode", "", nil)
|
|
|
|
|
+ var qr struct {
|
|
|
|
|
+ State string `json:"state"`
|
|
|
|
|
+ }
|
|
|
|
|
+ _ = json.Unmarshal(qrEnv.Data, &qr)
|
|
|
|
|
+
|
|
|
|
|
+ cbEnv := do(fx.t, app, http.MethodGet, "/auth/wechat/callback?code=code-x&state="+qr.State, "", nil)
|
|
|
|
|
+ if cbEnv.Code != 200 {
|
|
|
|
|
+ t.Fatalf("callback code=%d msg=%s(期望 200,unbound 语义)", cbEnv.Code, cbEnv.Message)
|
|
|
|
|
+ }
|
|
|
|
|
+ var cb struct {
|
|
|
|
|
+ Status string `json:"status"`
|
|
|
|
|
+ }
|
|
|
|
|
+ _ = json.Unmarshal(cbEnv.Data, &cb)
|
|
|
|
|
+ if cb.Status != "unbound" {
|
|
|
|
|
+ t.Fatalf("status=%q(期望 unbound)", cb.Status)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// TestWechatStatus_Placeholder 微信扫码状态占位:code 5001(暂未开放)。
|
|
|
|
|
-func TestWechatStatus_Placeholder(t *testing.T) {
|
|
|
|
|
|
|
+// TestWechatCallback_ThirdPartyFail 微信换 openid 失败:status=fail + code 4003(第三方超时/失败)。
|
|
|
|
|
+func TestWechatCallback_ThirdPartyFail(t *testing.T) {
|
|
|
fx := newFixture(t)
|
|
fx := newFixture(t)
|
|
|
|
|
+ fx.wechat.fail = true // 打桩:换 openid 失败
|
|
|
|
|
+
|
|
|
app := fx.newApp(nil)
|
|
app := fx.newApp(nil)
|
|
|
- env := do(fx.t, app, http.MethodGet, "/auth/wechat/status?state=abc", "", nil)
|
|
|
|
|
- if env.Code != apperrors.CodeInternal {
|
|
|
|
|
- t.Fatalf("code=%d(期望 %d 暂未开放)", env.Code, apperrors.CodeInternal)
|
|
|
|
|
|
|
+ qrEnv := do(fx.t, app, http.MethodGet, "/auth/wechat/qrcode", "", nil)
|
|
|
|
|
+ var qr struct {
|
|
|
|
|
+ State string `json:"state"`
|
|
|
|
|
+ }
|
|
|
|
|
+ _ = json.Unmarshal(qrEnv.Data, &qr)
|
|
|
|
|
+
|
|
|
|
|
+ cbEnv := do(fx.t, app, http.MethodGet, "/auth/wechat/callback?code=bad&state="+qr.State, "", nil)
|
|
|
|
|
+ if cbEnv.Code != apperrors.CodeThirdTimeout {
|
|
|
|
|
+ t.Fatalf("callback code=%d(期望 %d 第三方失败)", cbEnv.Code, apperrors.CodeThirdTimeout)
|
|
|
|
|
+ }
|
|
|
|
|
+ // state 应被置 fail(前端后续轮询读到 fail)
|
|
|
|
|
+ stEnv := do(fx.t, app, http.MethodGet, "/auth/wechat/status?state="+qr.State, "", nil)
|
|
|
|
|
+ var st struct {
|
|
|
|
|
+ Status string `json:"status"`
|
|
|
|
|
+ }
|
|
|
|
|
+ _ = json.Unmarshal(stEnv.Data, &st)
|
|
|
|
|
+ if st.Status != "fail" {
|
|
|
|
|
+ t.Fatalf("state 应置 fail,实际 %q", st.Status)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestWechatCallback_Replay 已终态 state 二次回调:code 1001(防重放)。
|
|
|
|
|
+func TestWechatCallback_Replay(t *testing.T) {
|
|
|
|
|
+ fx := newFixture(t)
|
|
|
|
|
+ uid := "01GAQF0Z6C1V9Y3E5H7K2M8B4T"
|
|
|
|
|
+ fx.addWechatUser(uid, "openid-bound")
|
|
|
|
|
+ fx.wechat.byCode["code-ok"] = "openid-bound"
|
|
|
|
|
+
|
|
|
|
|
+ app := fx.newApp(nil)
|
|
|
|
|
+ qrEnv := do(fx.t, app, http.MethodGet, "/auth/wechat/qrcode", "", nil)
|
|
|
|
|
+ var qr struct {
|
|
|
|
|
+ State string `json:"state"`
|
|
|
|
|
+ }
|
|
|
|
|
+ _ = json.Unmarshal(qrEnv.Data, &qr)
|
|
|
|
|
+
|
|
|
|
|
+ // 首次回调 → success(终态)
|
|
|
|
|
+ if env := do(fx.t, app, http.MethodGet, "/auth/wechat/callback?code=code-ok&state="+qr.State, "", nil); env.Code != 200 {
|
|
|
|
|
+ t.Fatalf("首次回调 code=%d(期望 200)", env.Code)
|
|
|
|
|
+ }
|
|
|
|
|
+ // 二次回同样 state → 1001(已终态,防重放)
|
|
|
|
|
+ env := do(fx.t, app, http.MethodGet, "/auth/wechat/callback?code=code-ok&state="+qr.State, "", nil)
|
|
|
|
|
+ if env.Code != apperrors.CodeUnauth {
|
|
|
|
|
+ t.Fatalf("重放 callback code=%d(期望 %d 已终态)", env.Code, apperrors.CodeUnauth)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestWechatCallback_MissingParam 缺 code 或 state:code 2001(参数校验)。
|
|
|
|
|
+func TestWechatCallback_MissingParam(t *testing.T) {
|
|
|
|
|
+ fx := newFixture(t)
|
|
|
|
|
+ app := fx.newApp(nil)
|
|
|
|
|
+ env := do(fx.t, app, http.MethodGet, "/auth/wechat/callback?code=only-code", "", nil)
|
|
|
|
|
+ if env.Code != apperrors.CodeBadParam {
|
|
|
|
|
+ t.Fatalf("code=%d(期望 %d 缺 state 参数)", env.Code, apperrors.CodeBadParam)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestWechat_NotConfigured wechat 服务未注入:qrcode/status/callback 均 code 4004(未配置)。
|
|
|
|
|
+func TestWechat_NotConfigured(t *testing.T) {
|
|
|
|
|
+ fx := newFixture(t)
|
|
|
|
|
+ // 构造一个 wechat 为 nil 的 handler(复用 fx.svc 之外的 AuthService,此处直接重造 Handler)
|
|
|
|
|
+ svc := fx.handler.svc // 复用现有 svc
|
|
|
|
|
+ h, err := New(Config{Svc: svc}) // Wechat 缺省 nil
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("New Handler: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ app := fiber.New(fiber.Config{DisableStartupMessage: true})
|
|
|
|
|
+ app.Get("/auth/wechat/qrcode", h.WechatQrcode)
|
|
|
|
|
+ app.Get("/auth/wechat/status", h.WechatStatus)
|
|
|
|
|
+ app.Get("/auth/wechat/callback", h.WechatCallback)
|
|
|
|
|
+
|
|
|
|
|
+ for _, path := range []string{"/auth/wechat/qrcode", "/auth/wechat/status?state=x", "/auth/wechat/callback?code=x&state=x"} {
|
|
|
|
|
+ env := do(fx.t, app, http.MethodGet, path, "", nil)
|
|
|
|
|
+ if env.Code != apperrors.CodeThirdNotConfigured {
|
|
|
|
|
+ t.Fatalf("%s code=%d(期望 %d 未配置)", path, env.Code, apperrors.CodeThirdNotConfigured)
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|