You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

138 lines
4.7 KiB

package mcp
import (
"testing"
"time"
"github.com/golang-jwt/jwt/v5"
)
// 签发在 user 模块、校验在 mcp 模块,两边隔着一个进程,
// 写错了不会编译报错,只会在真机上静默拒绝(或更糟:静默放行)。
// 这组用例把两边的口径钉在一起。
const testMcpKey = "test-mcp-key"
// signLikeUserModule 复刻 user/api_getmcptoken.go 的签发方式。
// ⚠️ 改那边的 claims 结构时,这里要一起改,否则这组用例就失去意义了。
func signLikeUserModule(t *testing.T, uid, audience string, ttl time.Duration, key string) string {
t.Helper()
claims := &jwt.RegisteredClaims{
ID: uid,
ExpiresAt: jwt.NewNumericDate(time.Now().Add(ttl)),
IssuedAt: jwt.NewNumericDate(time.Now()),
}
if audience != "" {
claims.Audience = jwt.ClaimStrings{audience}
}
s, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(key))
if err != nil {
t.Fatalf("签发失败: %v", err)
}
return s
}
func TestParseUIDWithAudience(t *testing.T) {
const uid = "2095078768153460736"
t.Run("正常令牌能解出uid", func(t *testing.T) {
tok := signLikeUserModule(t, uid, mcpTokenAudience, time.Hour, testMcpKey)
got, err := parseUIDWithAudience(tok, []byte(testMcpKey), mcpTokenAudience)
if err != nil {
t.Fatalf("不该失败: %v", err)
}
if got != uid {
t.Fatalf("uid 不对: got=%s want=%s", got, uid)
}
})
t.Run("换了密钥必须拒", func(t *testing.T) {
tok := signLikeUserModule(t, uid, mcpTokenAudience, time.Hour, "another-key")
if _, err := parseUIDWithAudience(tok, []byte(testMcpKey), mcpTokenAudience); err == nil {
t.Fatal("别人签的令牌居然通过了")
}
})
t.Run("用途不符必须拒", func(t *testing.T) {
// 这条守的是「将来若有别处也用 McpTokenKey 签东西,别被 MCP 一并接受」
tok := signLikeUserModule(t, uid, "someother", time.Hour, testMcpKey)
if _, err := parseUIDWithAudience(tok, []byte(testMcpKey), mcpTokenAudience); err == nil {
t.Fatal("用途不符的令牌居然通过了")
}
})
t.Run("没有用途标记必须拒", func(t *testing.T) {
tok := signLikeUserModule(t, uid, "", time.Hour, testMcpKey)
if _, err := parseUIDWithAudience(tok, []byte(testMcpKey), mcpTokenAudience); err == nil {
t.Fatal("没有 aud 的令牌居然通过了")
}
})
t.Run("过期必须拒", func(t *testing.T) {
tok := signLikeUserModule(t, uid, mcpTokenAudience, -time.Minute, testMcpKey)
if _, err := parseUIDWithAudience(tok, []byte(testMcpKey), mcpTokenAudience); err == nil {
t.Fatal("过期令牌居然通过了")
}
})
t.Run("没有过期时间必须拒", func(t *testing.T) {
// 没有 exp 的令牌等同长期有效,这套「短时效」的前提就没了
s, err := jwt.NewWithClaims(jwt.SigningMethodHS256, &jwt.RegisteredClaims{
ID: uid,
Audience: jwt.ClaimStrings{mcpTokenAudience},
}).SignedString([]byte(testMcpKey))
if err != nil {
t.Fatalf("签发失败: %v", err)
}
if _, err := parseUIDWithAudience(s, []byte(testMcpKey), mcpTokenAudience); err == nil {
t.Fatal("没有 exp 的令牌居然通过了")
}
})
t.Run("Bearer前缀要能兼容", func(t *testing.T) {
tok := signLikeUserModule(t, uid, mcpTokenAudience, time.Hour, testMcpKey)
if _, err := parseUIDWithAudience("Bearer "+tok, []byte(testMcpKey), mcpTokenAudience); err != nil {
t.Fatalf("带 Bearer 前缀不该失败: %v", err)
}
})
}
func TestResolveUIDWithToken(t *testing.T) {
const uid = "2095078768153460736"
m := &Mcp{}
m.options = &Options{McpTokenKey: testMcpKey}
t.Run("参数令牌可作为身份来源", func(t *testing.T) {
tok := signLikeUserModule(t, uid, mcpTokenAudience, time.Hour, testMcpKey)
got, err := m.ResolveUIDWithToken(t.Context(), "", tok)
if err != nil {
t.Fatalf("不该失败: %v", err)
}
if got != uid {
t.Fatalf("uid 不对: %s", got)
}
})
t.Run("参数uid与令牌不符必须拒", func(t *testing.T) {
tok := signLikeUserModule(t, uid, mcpTokenAudience, time.Hour, testMcpKey)
if _, err := m.ResolveUIDWithToken(t.Context(), "9999", tok); err == nil {
t.Fatal("冒充别人居然通过了")
}
})
t.Run("只给裸uid不给令牌必须拒", func(t *testing.T) {
// 这条是整层鉴权的底线:裸 uid 谁都能填,绝不能作为身份来源
if _, err := m.ResolveUIDWithToken(t.Context(), uid, ""); err == nil {
t.Fatal("只凭一个 uid 参数就放行了")
}
})
t.Run("没配密钥时带令牌也要拒", func(t *testing.T) {
empty := &Mcp{}
empty.options = &Options{}
tok := signLikeUserModule(t, uid, mcpTokenAudience, time.Hour, testMcpKey)
if _, err := empty.ResolveUIDWithToken(t.Context(), "", tok); err == nil {
t.Fatal("密钥没配却放行了")
}
})
}