diff --git a/internal/mcp/auth_middleware.go b/internal/mcp/auth_middleware.go index a58c4e0..afb51b2 100644 --- a/internal/mcp/auth_middleware.go +++ b/internal/mcp/auth_middleware.go @@ -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 转换) diff --git a/internal/mcp/auth_middleware_test.go b/internal/mcp/auth_middleware_test.go index e93a8d7..a4ff7f5 100644 --- a/internal/mcp/auth_middleware_test.go +++ b/internal/mcp/auth_middleware_test.go @@ -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) { diff --git a/internal/mcp/domain.go b/internal/mcp/domain.go index 6028044..6b80da9 100644 --- a/internal/mcp/domain.go +++ b/internal/mcp/domain.go @@ -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 } diff --git a/internal/mcp/repository.go b/internal/mcp/repository.go index 1678a77..dd52634 100644 --- a/internal/mcp/repository.go +++ b/internal/mcp/repository.go @@ -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 } diff --git a/internal/mcp/repository_test.go b/internal/mcp/repository_test.go index 43e329a..e2057c9 100644 --- a/internal/mcp/repository_test.go +++ b/internal/mcp/repository_test.go @@ -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}}, diff --git a/internal/mcp/token_service.go b/internal/mcp/token_service.go index d276042..279c199 100644 --- a/internal/mcp/token_service.go +++ b/internal/mcp/token_service.go @@ -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") } diff --git a/internal/mcp/token_service_test.go b/internal/mcp/token_service_test.go index 798c4e0..0105b2a 100644 --- a/internal/mcp/token_service_test.go +++ b/internal/mcp/token_service_test.go @@ -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