refactor(mcp): apply Part 1 review I1+I3+I4+I5

- Rename MCPToken -> Token (mcp.Token reads cleaner; zero external callers yet)
- IssueSystemToken: guard against nil ACPSessionID / UserID (early input validation)
- TokenService: inject now func for clock injection (parity with RateLimiter)
- Repository CHECK constraint tests: assert wrapped CodeInternal

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-08 10:28:49 +08:00
parent 980c6fcf0e
commit a00f5a9efd
7 changed files with 106 additions and 65 deletions
+1 -1
View File
@@ -17,7 +17,7 @@ type authCtxKey struct{}
var AuthContextKey = authCtxKey{}
// AuthSession 是中间件解析后注入 ctx 的鉴权快照。
// 不直接暴露 *MCPToken(隐藏 hash 字段,避免下游误用)。
// 不直接暴露 *Token(隐藏 hash 字段,避免下游误用)。
type AuthSession struct {
TokenID string // 用于审计 metadata 写入
UserID string // user_id 字符串形式(避免反复 uuid.UUID -> string 转换)
+4 -4
View File
@@ -18,7 +18,7 @@ import (
func TestAuthMiddleware_BearerHeader(t *testing.T) {
t.Parallel()
repo := newFakeRepo()
svc := mcp.NewTokenService(repo, &fakeAudit{})
svc := mcp.NewTokenService(repo, &fakeAudit{}, nil)
res, err := svc.IssueAdmin(context.Background(), mcp.IssueAdminInput{
UserID: uuid.New(),
@@ -49,7 +49,7 @@ func TestAuthMiddleware_BearerHeader(t *testing.T) {
func TestAuthMiddleware_QueryFallback(t *testing.T) {
t.Parallel()
repo := newFakeRepo()
svc := mcp.NewTokenService(repo, &fakeAudit{})
svc := mcp.NewTokenService(repo, &fakeAudit{}, nil)
res, _ := svc.IssueAdmin(context.Background(), mcp.IssueAdminInput{
UserID: uuid.New(),
@@ -72,7 +72,7 @@ func TestAuthMiddleware_QueryFallback(t *testing.T) {
func TestAuthMiddleware_MissingToken(t *testing.T) {
t.Parallel()
svc := mcp.NewTokenService(newFakeRepo(), &fakeAudit{})
svc := mcp.NewTokenService(newFakeRepo(), &fakeAudit{}, nil)
mw := mcp.NewAuthMiddleware(svc)
h := mw(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -87,7 +87,7 @@ func TestAuthMiddleware_MissingToken(t *testing.T) {
func TestAuthMiddleware_InvalidToken(t *testing.T) {
t.Parallel()
svc := mcp.NewTokenService(newFakeRepo(), &fakeAudit{})
svc := mcp.NewTokenService(newFakeRepo(), &fakeAudit{}, nil)
mw := mcp.NewAuthMiddleware(svc)
h := mw(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+4 -4
View File
@@ -10,7 +10,7 @@
// - 工具/prompts/resources 的业务桥接(调 project / chat 现有 service)
//
// 三类核心抽象:
// - MCPToken:DB 行记录,token plaintext 仅签发时返回一次
// - Token:DB 行记录,token plaintext 仅签发时返回一次
// - Scope:JSONB 形态的工具白名单 + project_ids 限定
// - Issuer:'system' 或 'admin',决定生命期与是否绑定 ACP session
package mcp
@@ -76,11 +76,11 @@ func (s *Scope) AllowsProject(id uuid.UUID) bool {
return false
}
// MCPToken 是 mcp_tokens 表的 Go 表示。
// Token 是 mcp_tokens 表的 Go 表示。
//
// TokenHash 是 SHA-256(plaintext);plaintext 仅在 IssueAdmin/IssueSystemToken
// 的返回值里出现一次,DB 与本结构都不持有 plaintext。
type MCPToken struct {
type Token struct {
ID uuid.UUID
TokenHash []byte
UserID uuid.UUID
@@ -97,7 +97,7 @@ type MCPToken struct {
// IsActive 报告 token 当前是否可用(未撤销 + 未过期)。
// now 显式注入便于测试;生产调用方传 time.Now()。
func (t *MCPToken) IsActive(now time.Time) bool {
func (t *Token) IsActive(now time.Time) bool {
if t.RevokedAt != nil {
return false
}
+16 -16
View File
@@ -18,10 +18,10 @@ import (
// Repository 是 mcp 模块对 PG 的全部依赖。Service 单测通过手写 fakeRepo 满足。
type Repository interface {
// CRUD
Create(ctx context.Context, t *MCPToken) (*MCPToken, error)
GetByHash(ctx context.Context, hash []byte) (*MCPToken, error)
GetByID(ctx context.Context, id uuid.UUID) (*MCPToken, error)
List(ctx context.Context, callerUserID *uuid.UUID, activeOnly bool) ([]*MCPToken, error)
Create(ctx context.Context, t *Token) (*Token, error)
GetByHash(ctx context.Context, hash []byte) (*Token, error)
GetByID(ctx context.Context, id uuid.UUID) (*Token, error)
List(ctx context.Context, callerUserID *uuid.UUID, activeOnly bool) ([]*Token, error)
// 状态变更
UpdateLastUsed(ctx context.Context, id uuid.UUID) error
@@ -99,13 +99,13 @@ func scopeFromJSON(raw []byte) (Scope, error) {
return s, nil
}
// rowToMCPToken 把 sqlc 生成的 row 翻译为业务结构。
func rowToMCPToken(r mcpsqlc.McpToken) (*MCPToken, error) {
// rowToToken 把 sqlc 生成的 row 翻译为业务结构。
func rowToToken(r mcpsqlc.McpToken) (*Token, error) {
scope, err := scopeFromJSON(r.Scope)
if err != nil {
return nil, errs.Wrap(err, errs.CodeInternal, "decode mcp_token.scope")
}
return &MCPToken{
return &Token{
ID: fromPgUUID(r.ID),
TokenHash: r.TokenHash,
UserID: fromPgUUID(r.UserID),
@@ -131,7 +131,7 @@ func pgxNoRowsToErrNotFound(err error, code errs.Code, msg string) error {
// ===== Create =====
func (r *pgRepo) Create(ctx context.Context, t *MCPToken) (*MCPToken, error) {
func (r *pgRepo) Create(ctx context.Context, t *Token) (*Token, error) {
scopeJSON, err := scopeToJSON(t.Scope)
if err != nil {
return nil, errs.Wrap(err, errs.CodeInternal, "encode mcp_token.scope")
@@ -150,30 +150,30 @@ func (r *pgRepo) Create(ctx context.Context, t *MCPToken) (*MCPToken, error) {
if err != nil {
return nil, errs.Wrap(err, errs.CodeInternal, "insert mcp_token")
}
return rowToMCPToken(row)
return rowToToken(row)
}
// ===== Get =====
func (r *pgRepo) GetByHash(ctx context.Context, hash []byte) (*MCPToken, error) {
func (r *pgRepo) GetByHash(ctx context.Context, hash []byte) (*Token, error) {
row, err := r.q.GetMcpTokenByHash(ctx, hash)
if err != nil {
return nil, pgxNoRowsToErrNotFound(err, errs.CodeMcpTokenInvalid, "mcp token not found")
}
return rowToMCPToken(row)
return rowToToken(row)
}
func (r *pgRepo) GetByID(ctx context.Context, id uuid.UUID) (*MCPToken, error) {
func (r *pgRepo) GetByID(ctx context.Context, id uuid.UUID) (*Token, error) {
row, err := r.q.GetMcpTokenByID(ctx, toPgUUID(id))
if err != nil {
return nil, pgxNoRowsToErrNotFound(err, errs.CodeMcpTokenNotFound, "mcp token not found")
}
return rowToMCPToken(row)
return rowToToken(row)
}
// ===== Stub methods (implemented in subsequent tasks) =====
func (r *pgRepo) List(ctx context.Context, callerUserID *uuid.UUID, activeOnly bool) ([]*MCPToken, error) {
func (r *pgRepo) List(ctx context.Context, callerUserID *uuid.UUID, activeOnly bool) ([]*Token, error) {
rows, err := r.q.ListMcpTokens(ctx, mcpsqlc.ListMcpTokensParams{
CallerUserID: toPgUUIDPtr(callerUserID),
ActiveOnly: activeOnly,
@@ -181,9 +181,9 @@ func (r *pgRepo) List(ctx context.Context, callerUserID *uuid.UUID, activeOnly b
if err != nil {
return nil, errs.Wrap(err, errs.CodeInternal, "list mcp_tokens")
}
out := make([]*MCPToken, 0, len(rows))
out := make([]*Token, 0, len(rows))
for _, row := range rows {
t, err := rowToMCPToken(row)
t, err := rowToToken(row)
if err != nil {
return nil, err
}
+16 -11
View File
@@ -62,7 +62,7 @@ func TestPgRepo_AdminToken_CRUD(t *testing.T) {
require.NoError(t, err)
expires := time.Now().Add(90 * 24 * time.Hour)
created, err := repo.Create(ctx, &mcp.MCPToken{
created, err := repo.Create(ctx, &mcp.Token{
ID: uuid.New(),
TokenHash: hash,
UserID: uid,
@@ -133,12 +133,14 @@ func TestPgRepo_CheckConstraint_SystemRequiresSession(t *testing.T) {
require.NoError(t, err)
// system token + 没 acp_session_id → CHECK 拒绝
_, err = repo.Create(ctx, &mcp.MCPToken{
_, err = repo.Create(ctx, &mcp.Token{
ID: uuid.New(), TokenHash: hash, UserID: uid,
Name: "x", Issuer: mcp.IssuerSystem, CreatedBy: uid,
})
require.Error(t, err)
// PG 抛 23514 check_violation;包装在 errs.CodeInternal 里
ae, ok := errs.As(err)
require.True(t, ok)
assert.Equal(t, errs.CodeInternal, ae.Code, "CHECK violation should wrap to CodeInternal")
}
func TestPgRepo_CheckConstraint_AdminMustNotHaveSession(t *testing.T) {
@@ -152,12 +154,15 @@ func TestPgRepo_CheckConstraint_AdminMustNotHaveSession(t *testing.T) {
fakeSessionID := uuid.New()
// admin token + 有 acp_session_id → CHECK 拒绝
_, err = repo.Create(ctx, &mcp.MCPToken{
_, err = repo.Create(ctx, &mcp.Token{
ID: uuid.New(), TokenHash: hash, UserID: uid,
Name: "y", Issuer: mcp.IssuerAdmin, ACPSessionID: &fakeSessionID,
CreatedBy: uid,
})
require.Error(t, err)
ae, ok := errs.As(err)
require.True(t, ok)
assert.Equal(t, errs.CodeInternal, ae.Code, "CHECK violation should wrap to CodeInternal")
}
func TestPgRepo_List_FilterByCaller(t *testing.T) {
@@ -171,7 +176,7 @@ func TestPgRepo_List_FilterByCaller(t *testing.T) {
mkAdmin := func(uid uuid.UUID, name string) {
_, hash, _ := mcp.NewToken()
_, err := repo.Create(ctx, &mcp.MCPToken{
_, err := repo.Create(ctx, &mcp.Token{
ID: uuid.New(), TokenHash: hash, UserID: uid, Name: name,
Issuer: mcp.IssuerAdmin, CreatedBy: admin,
})
@@ -208,7 +213,7 @@ func TestPgRepo_List_ActiveOnly(t *testing.T) {
expPast := time.Now().Add(-1 * time.Hour)
mk := func(name string, exp *time.Time) uuid.UUID {
_, hash, _ := mcp.NewToken()
tok, err := repo.Create(ctx, &mcp.MCPToken{
tok, err := repo.Create(ctx, &mcp.Token{
ID: uuid.New(), TokenHash: hash, UserID: user, Name: name,
Issuer: mcp.IssuerAdmin, ExpiresAt: exp, CreatedBy: admin,
})
@@ -245,7 +250,7 @@ func TestPgRepo_PurgeExpired(t *testing.T) {
// 一个早已过期的 row(手工写 expires_at = 30d ago)
_, hash, _ := mcp.NewToken()
tk, err := repo.Create(ctx, &mcp.MCPToken{
tk, err := repo.Create(ctx, &mcp.Token{
ID: uuid.New(), TokenHash: hash, UserID: user, Name: "old",
Issuer: mcp.IssuerAdmin, ExpiresAt: &old, CreatedBy: admin,
})
@@ -253,7 +258,7 @@ func TestPgRepo_PurgeExpired(t *testing.T) {
// 一个新 token
_, hash2, _ := mcp.NewToken()
_, err = repo.Create(ctx, &mcp.MCPToken{
_, err = repo.Create(ctx, &mcp.Token{
ID: uuid.New(), TokenHash: hash2, UserID: user, Name: "new",
Issuer: mcp.IssuerAdmin, ExpiresAt: &expFuture, CreatedBy: admin,
})
@@ -323,7 +328,7 @@ func TestPgRepo_RevokeBySession(t *testing.T) {
// 插一个 system token
_, hash, _ := mcp.NewToken()
tk, err := repo.Create(ctx, &mcp.MCPToken{
tk, err := repo.Create(ctx, &mcp.Token{
ID: uuid.New(), TokenHash: hash, UserID: uid,
Name: "system-x", Issuer: mcp.IssuerSystem,
Scope: mcp.Scope{ProjectIDs: []uuid.UUID{pid}},
@@ -359,7 +364,7 @@ func TestPgRepo_ReapStaleSystemTokens(t *testing.T) {
// 一个 crashed session 的 system token
sidCrashed, pidA := mustInsertACPSetup(t, ctx, pool, uid, "crashed")
_, h1, _ := mcp.NewToken()
tk1, err := repo.Create(ctx, &mcp.MCPToken{
tk1, err := repo.Create(ctx, &mcp.Token{
ID: uuid.New(), TokenHash: h1, UserID: uid,
Name: "crashed-tok", Issuer: mcp.IssuerSystem,
Scope: mcp.Scope{ProjectIDs: []uuid.UUID{pidA}},
@@ -370,7 +375,7 @@ func TestPgRepo_ReapStaleSystemTokens(t *testing.T) {
// 一个 running session 的 system token(不该被 reap)
sidRunning, pidB := mustInsertACPSetup(t, ctx, pool, uid, "running")
_, h2, _ := mcp.NewToken()
tk2, err := repo.Create(ctx, &mcp.MCPToken{
tk2, err := repo.Create(ctx, &mcp.Token{
ID: uuid.New(), TokenHash: h2, UserID: uid,
Name: "running-tok", Issuer: mcp.IssuerSystem,
Scope: mcp.Scope{ProjectIDs: []uuid.UUID{pidB}},
+21 -11
View File
@@ -20,7 +20,7 @@ import (
type TokenService interface {
IssueAdmin(ctx context.Context, in IssueAdminInput) (IssueResult, error)
IssueSystemToken(ctx context.Context, in IssueSystemTokenInput) (IssueResult, error)
Authenticate(ctx context.Context, plaintext string) (*MCPToken, error)
Authenticate(ctx context.Context, plaintext string) (*Token, error)
Revoke(ctx context.Context, id, byUserID uuid.UUID) error
RevokeBySession(ctx context.Context, sessionID uuid.UUID) error
}
@@ -60,14 +60,18 @@ var defaultSystemTools = []string{
"create_requirement", "update_requirement", "close_requirement", "reopen_requirement", "set_requirement_phase",
}
// NewTokenService 构造 service。
func NewTokenService(repo Repository, audit audit.Recorder) TokenService {
return &tokenService{repo: repo, audit: audit}
// NewTokenService 构造 service。now 用于测试注入;nil 时默认 time.Now。
func NewTokenService(repo Repository, audit audit.Recorder, now func() time.Time) TokenService {
if now == nil {
now = time.Now
}
return &tokenService{repo: repo, audit: audit, now: now}
}
type tokenService struct {
repo Repository
audit audit.Recorder
now func() time.Time
}
// IssueAdmin 签发一个长期 admin token。Name + ExpiresAt 必填;scope 可空(= 全权)。
@@ -79,7 +83,7 @@ func (s *tokenService) IssueAdmin(ctx context.Context, in IssueAdminInput) (Issu
if in.ExpiresAt.IsZero() {
return IssueResult{}, errs.New(errs.CodeMcpTokenExpiresRequired, "expires_at is required for admin token")
}
if !in.ExpiresAt.After(time.Now()) {
if !in.ExpiresAt.After(s.now()) {
return IssueResult{}, errs.New(errs.CodeMcpTokenExpiresRequired, "expires_at must be in the future")
}
@@ -89,7 +93,7 @@ func (s *tokenService) IssueAdmin(ctx context.Context, in IssueAdminInput) (Issu
}
id := uuid.New()
created, err := s.repo.Create(ctx, &MCPToken{
created, err := s.repo.Create(ctx, &Token{
ID: id,
TokenHash: hash,
UserID: in.UserID,
@@ -140,6 +144,12 @@ func (s *tokenService) IssueAdmin(ctx context.Context, in IssueAdminInput) (Issu
// 默认 ExpiresIn = 24h;默认 Tools 为内置 PM 工具白名单(spec §6.2)。
// 写一条 audit `mcp.token.create_system`。
func (s *tokenService) IssueSystemToken(ctx context.Context, in IssueSystemTokenInput) (IssueResult, error) {
if in.ACPSessionID == uuid.Nil {
return IssueResult{}, errs.New(errs.CodeInvalidInput, "acp_session_id is required for system token")
}
if in.UserID == uuid.Nil {
return IssueResult{}, errs.New(errs.CodeInvalidInput, "user_id is required for system token")
}
if in.ExpiresIn <= 0 {
in.ExpiresIn = 24 * time.Hour
}
@@ -148,7 +158,7 @@ func (s *tokenService) IssueSystemToken(ctx context.Context, in IssueSystemToken
// 用 copy 避免共享 defaultSystemTools 给调用方修改
tools = append([]string{}, defaultSystemTools...)
}
expires := time.Now().Add(in.ExpiresIn)
expires := s.now().Add(in.ExpiresIn)
plain, hash, err := NewToken()
if err != nil {
@@ -156,7 +166,7 @@ func (s *tokenService) IssueSystemToken(ctx context.Context, in IssueSystemToken
}
id := uuid.New()
created, err := s.repo.Create(ctx, &MCPToken{
created, err := s.repo.Create(ctx, &Token{
ID: id,
TokenHash: hash,
UserID: in.UserID,
@@ -200,8 +210,8 @@ func (s *tokenService) IssueSystemToken(ctx context.Context, in IssueSystemToken
// - revoked_at 非空 → CodeMcpTokenRevoked (401)
// - expires_at 已过 → CodeMcpTokenExpired (401)
//
// 返回的 *MCPToken 不含 plaintext;调用方从中读 user_id + scope。
func (s *tokenService) Authenticate(ctx context.Context, plaintext string) (*MCPToken, error) {
// 返回的 *Token 不含 plaintext;调用方从中读 user_id + scope。
func (s *tokenService) Authenticate(ctx context.Context, plaintext string) (*Token, error) {
if strings.TrimSpace(plaintext) == "" {
return nil, errs.New(errs.CodeMcpTokenInvalid, "missing token")
}
@@ -212,7 +222,7 @@ func (s *tokenService) Authenticate(ctx context.Context, plaintext string) (*MCP
if tok.RevokedAt != nil {
return nil, errs.New(errs.CodeMcpTokenRevoked, "token revoked")
}
if tok.ExpiresAt != nil && !tok.ExpiresAt.After(time.Now()) {
if tok.ExpiresAt != nil && !tok.ExpiresAt.After(s.now()) {
return nil, errs.New(errs.CodeMcpTokenExpired, "token expired")
}
+44 -18
View File
@@ -20,18 +20,18 @@ import (
type fakeRepo struct {
mu sync.Mutex
tokens map[uuid.UUID]*mcp.MCPToken
tokens map[uuid.UUID]*mcp.Token
byHash map[string]uuid.UUID
}
func newFakeRepo() *fakeRepo {
return &fakeRepo{
tokens: map[uuid.UUID]*mcp.MCPToken{},
tokens: map[uuid.UUID]*mcp.Token{},
byHash: map[string]uuid.UUID{},
}
}
func (r *fakeRepo) Create(ctx context.Context, t *mcp.MCPToken) (*mcp.MCPToken, error) {
func (r *fakeRepo) Create(ctx context.Context, t *mcp.Token) (*mcp.Token, error) {
r.mu.Lock()
defer r.mu.Unlock()
c := *t
@@ -40,7 +40,7 @@ func (r *fakeRepo) Create(ctx context.Context, t *mcp.MCPToken) (*mcp.MCPToken,
r.byHash[string(c.TokenHash)] = c.ID
return &c, nil
}
func (r *fakeRepo) GetByHash(ctx context.Context, h []byte) (*mcp.MCPToken, error) {
func (r *fakeRepo) GetByHash(ctx context.Context, h []byte) (*mcp.Token, error) {
r.mu.Lock()
defer r.mu.Unlock()
id, ok := r.byHash[string(h)]
@@ -50,7 +50,7 @@ func (r *fakeRepo) GetByHash(ctx context.Context, h []byte) (*mcp.MCPToken, erro
c := *r.tokens[id]
return &c, nil
}
func (r *fakeRepo) GetByID(ctx context.Context, id uuid.UUID) (*mcp.MCPToken, error) {
func (r *fakeRepo) GetByID(ctx context.Context, id uuid.UUID) (*mcp.Token, error) {
r.mu.Lock()
defer r.mu.Unlock()
t, ok := r.tokens[id]
@@ -60,10 +60,10 @@ func (r *fakeRepo) GetByID(ctx context.Context, id uuid.UUID) (*mcp.MCPToken, er
c := *t
return &c, nil
}
func (r *fakeRepo) List(ctx context.Context, caller *uuid.UUID, activeOnly bool) ([]*mcp.MCPToken, error) {
func (r *fakeRepo) List(ctx context.Context, caller *uuid.UUID, activeOnly bool) ([]*mcp.Token, error) {
r.mu.Lock()
defer r.mu.Unlock()
var out []*mcp.MCPToken
var out []*mcp.Token
for _, t := range r.tokens {
if caller != nil && (t.UserID != *caller || t.Issuer != mcp.IssuerAdmin) {
continue
@@ -151,7 +151,7 @@ func TestIssueAdmin_HappyPath(t *testing.T) {
t.Parallel()
repo := newFakeRepo()
au := &fakeAudit{}
svc := mcp.NewTokenService(repo, au)
svc := mcp.NewTokenService(repo, au, nil)
uid := uuid.New()
in := mcp.IssueAdminInput{
@@ -169,7 +169,7 @@ func TestIssueAdmin_HappyPath(t *testing.T) {
func TestIssueAdmin_NameRequired(t *testing.T) {
t.Parallel()
svc := mcp.NewTokenService(newFakeRepo(), &fakeAudit{})
svc := mcp.NewTokenService(newFakeRepo(), &fakeAudit{}, nil)
_, err := svc.IssueAdmin(context.Background(), mcp.IssueAdminInput{
UserID: uuid.New(),
@@ -184,7 +184,7 @@ func TestIssueAdmin_NameRequired(t *testing.T) {
func TestIssueAdmin_ExpiresRequired(t *testing.T) {
t.Parallel()
svc := mcp.NewTokenService(newFakeRepo(), &fakeAudit{})
svc := mcp.NewTokenService(newFakeRepo(), &fakeAudit{}, nil)
_, err := svc.IssueAdmin(context.Background(), mcp.IssueAdminInput{
UserID: uuid.New(),
@@ -198,7 +198,7 @@ func TestIssueAdmin_ExpiresRequired(t *testing.T) {
func TestIssueAdmin_ExpiresMustBeFuture(t *testing.T) {
t.Parallel()
svc := mcp.NewTokenService(newFakeRepo(), &fakeAudit{})
svc := mcp.NewTokenService(newFakeRepo(), &fakeAudit{}, nil)
_, err := svc.IssueAdmin(context.Background(), mcp.IssueAdminInput{
UserID: uuid.New(),
@@ -215,7 +215,7 @@ func TestIssueSystemToken_Defaults(t *testing.T) {
t.Parallel()
repo := newFakeRepo()
au := &fakeAudit{}
svc := mcp.NewTokenService(repo, au)
svc := mcp.NewTokenService(repo, au, nil)
uid := uuid.New()
sid := uuid.New()
@@ -243,7 +243,7 @@ func TestIssueSystemToken_Defaults(t *testing.T) {
func TestAuthenticate_HappyPath(t *testing.T) {
t.Parallel()
repo := newFakeRepo()
svc := mcp.NewTokenService(repo, &fakeAudit{})
svc := mcp.NewTokenService(repo, &fakeAudit{}, nil)
res, err := svc.IssueAdmin(context.Background(), mcp.IssueAdminInput{
UserID: uuid.New(),
@@ -260,7 +260,7 @@ func TestAuthenticate_HappyPath(t *testing.T) {
func TestAuthenticate_Empty(t *testing.T) {
t.Parallel()
svc := mcp.NewTokenService(newFakeRepo(), &fakeAudit{})
svc := mcp.NewTokenService(newFakeRepo(), &fakeAudit{}, nil)
_, err := svc.Authenticate(context.Background(), "")
ae, ok := errs.As(err)
@@ -271,7 +271,7 @@ func TestAuthenticate_Empty(t *testing.T) {
func TestAuthenticate_Revoked(t *testing.T) {
t.Parallel()
repo := newFakeRepo()
svc := mcp.NewTokenService(repo, &fakeAudit{})
svc := mcp.NewTokenService(repo, &fakeAudit{}, nil)
res, _ := svc.IssueAdmin(context.Background(), mcp.IssueAdminInput{
UserID: uuid.New(),
@@ -290,7 +290,7 @@ func TestAuthenticate_Revoked(t *testing.T) {
func TestAuthenticate_Expired(t *testing.T) {
t.Parallel()
repo := newFakeRepo()
svc := mcp.NewTokenService(repo, &fakeAudit{})
svc := mcp.NewTokenService(repo, &fakeAudit{}, nil)
res, _ := svc.IssueAdmin(context.Background(), mcp.IssueAdminInput{
UserID: uuid.New(),
@@ -310,7 +310,7 @@ func TestAuthenticate_Expired(t *testing.T) {
func TestAuthenticate_TamperedToken(t *testing.T) {
t.Parallel()
svc := mcp.NewTokenService(newFakeRepo(), &fakeAudit{})
svc := mcp.NewTokenService(newFakeRepo(), &fakeAudit{}, nil)
_, err := svc.Authenticate(context.Background(), "mcpt_tampered_value")
ae, ok := errs.As(err)
@@ -322,7 +322,7 @@ func TestRevokeBySession_AuditPerToken(t *testing.T) {
t.Parallel()
repo := newFakeRepo()
au := &fakeAudit{}
svc := mcp.NewTokenService(repo, au)
svc := mcp.NewTokenService(repo, au, nil)
uid := uuid.New()
sid := uuid.New()
@@ -352,5 +352,31 @@ func TestRevokeBySession_AuditPerToken(t *testing.T) {
assert.Equal(t, 3, revokeCnt)
}
func TestIssueSystemToken_NilACPSessionID(t *testing.T) {
t.Parallel()
svc := mcp.NewTokenService(newFakeRepo(), &fakeAudit{}, nil)
_, err := svc.IssueSystemToken(context.Background(), mcp.IssueSystemTokenInput{
UserID: uuid.New(),
ACPSessionID: uuid.Nil,
})
ae, ok := errs.As(err)
require.True(t, ok)
assert.Equal(t, errs.CodeInvalidInput, ae.Code)
}
func TestIssueSystemToken_NilUserID(t *testing.T) {
t.Parallel()
svc := mcp.NewTokenService(newFakeRepo(), &fakeAudit{}, nil)
_, err := svc.IssueSystemToken(context.Background(), mcp.IssueSystemTokenInput{
UserID: uuid.Nil,
ACPSessionID: uuid.New(),
})
ae, ok := errs.As(err)
require.True(t, ok)
assert.Equal(t, errs.CodeInvalidInput, ae.Code)
}
// 编译期保证:类型实现接口
var _ = errors.New