115 lines
3.7 KiB
Go
115 lines
3.7 KiB
Go
package identity
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"server/pkg/jwtutil"
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
)
|
|
|
|
// 这些用例只覆盖不依赖数据库的身份判定:端归属、令牌错误分类、状态码映射。
|
|
|
|
func mint(t *testing.T, userType string) string {
|
|
t.Helper()
|
|
tok, err := jwtutil.GenerateToken(4, "hero920103", 2, userType)
|
|
if err != nil {
|
|
t.Fatalf("签发测试令牌失败: %v", err)
|
|
}
|
|
return tok
|
|
}
|
|
|
|
func requestWith(auth string) *http.Request {
|
|
r := httptest.NewRequest(http.MethodGet, "/backend/modules/getTenantList", nil)
|
|
if auth != "" {
|
|
r.Header.Set("Authorization", auth)
|
|
}
|
|
return r
|
|
}
|
|
|
|
func TestRequireAcceptsMatchingEnd(t *testing.T) {
|
|
id, err := Require(requestWith("Bearer "+mint(t, string(EndBackend))), EndBackend)
|
|
if err != nil {
|
|
t.Fatalf("backend 令牌应放行,实际报错 %v", err)
|
|
}
|
|
if id.End != EndBackend || id.UserID != 4 || id.TenantID != 2 || id.Username != "hero920103" {
|
|
t.Fatalf("身份解析结果不符:%+v", id)
|
|
}
|
|
}
|
|
|
|
func TestRequireRejectsForeignEnd(t *testing.T) {
|
|
// 认证中心换码路径曾签发 user_type=tenant,落到这里就是「令牌有效但端不匹配」。
|
|
// 这类失败续期救不回来,必须是 403,不能触发前端静默续期重放。
|
|
for _, userType := range []string{"tenant", "user", "employee", "platform", ""} {
|
|
_, err := Require(requestWith("Bearer "+mint(t, userType)), EndBackend)
|
|
if !errors.Is(err, ErrWrongEnd) {
|
|
t.Fatalf("user_type=%q 应判为端不匹配,实际 %v", userType, err)
|
|
}
|
|
if got := Status(err); got != http.StatusForbidden {
|
|
t.Fatalf("user_type=%q 期望 403,实际 %d", userType, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRequireAllowsMultipleEnds(t *testing.T) {
|
|
r := requestWith("Bearer " + mint(t, string(EndApp)))
|
|
if _, err := Require(r, EndBackend, EndApp); err != nil {
|
|
t.Fatalf("跨端接口应接受 app 令牌,实际 %v", err)
|
|
}
|
|
if _, err := Require(requestWith("Bearer "+mint(t, string(EndApp))), EndBackend); !errors.Is(err, ErrWrongEnd) {
|
|
t.Fatalf("单端接口不应接受 app 令牌,实际 %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRequireTokenErrorsAreUnauthorized(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
auth string
|
|
want error
|
|
}{
|
|
{"无令牌", "", ErrNoToken},
|
|
{"非 Bearer", "Token abc", ErrBadFormat},
|
|
{"缺载荷", "Bearer", ErrBadFormat},
|
|
{"乱码令牌", "Bearer aaa.bbb.ccc", ErrInvalid},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
_, err := Require(requestWith(c.auth), EndBackend)
|
|
if !errors.Is(err, c.want) {
|
|
t.Fatalf("期望 %v,实际 %v", c.want, err)
|
|
}
|
|
if got := Status(err); got != http.StatusUnauthorized {
|
|
t.Fatalf("令牌类错误应返 401 让前端续期,实际 %d", got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestClassifyExpired(t *testing.T) {
|
|
// SignToken/GenerateToken 都把非正数 TTL 钳成默认值,造不出真实过期令牌,
|
|
// 因此这里直接验证错误分类:被包装的过期错误也要认出来。
|
|
wrapped := fmt.Errorf("parse token: %w", jwt.ErrTokenExpired)
|
|
if got := classify(wrapped); !errors.Is(got, ErrExpired) {
|
|
t.Fatalf("包装后的过期错误应归类为 ErrExpired,实际 %v", got)
|
|
}
|
|
if got := classify(errors.New("token is expired by 1h")); !errors.Is(got, ErrExpired) {
|
|
t.Fatalf("裸字符串过期错误应归类为 ErrExpired,实际 %v", got)
|
|
}
|
|
if got := classify(jwt.ErrTokenSignatureInvalid); !errors.Is(got, ErrInvalid) {
|
|
t.Fatalf("签名错误应归类为 ErrInvalid,实际 %v", got)
|
|
}
|
|
if got := Status(ErrExpired); got != http.StatusUnauthorized {
|
|
t.Fatalf("过期应返 401,实际 %d", got)
|
|
}
|
|
}
|
|
|
|
func TestRequireNilRequest(t *testing.T) {
|
|
if _, err := Require(nil, EndBackend); !errors.Is(err, ErrNoToken) {
|
|
t.Fatalf("空请求应判为未提供认证信息,实际 %v", err)
|
|
}
|
|
}
|