You've already forked agentic-coding-workflow
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:
@@ -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 转换)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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}},
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user