11import { expect , test , vi , beforeEach , describe } from 'vitest' ;
22import { MOCK_REFRESH_TOKEN , MOCK_USER_WITH_ACCOUNTS , prisma } from '@/__mocks__/prisma' ;
3- import { verifyAndExchangeCode , verifyAndRotateRefreshToken , revokeToken } from './server' ;
3+ import { generateAndStoreAuthCode , verifyAndExchangeCode , verifyAndRotateRefreshToken , revokeToken } from './server' ;
44import { SOURCEBOT_MCP_OAUTH_SCOPE } from './constants' ;
55
66vi . mock ( '@/prisma' , async ( ) => {
@@ -31,6 +31,7 @@ const VALID_AUTH_CODE = {
3131 redirectUri : 'http://localhost:9999/callback' ,
3232 // SHA-256('myverifier') base64url
3333 codeChallenge : 'Eb223qLjTQNFkRjCVsrDbsBk5ycPKwHdbHNRX99tTeQ' ,
34+ scope : SOURCEBOT_MCP_OAUTH_SCOPE ,
3435 resource : null ,
3536 dpopJkt : null ,
3637 expiresAt : new Date ( Date . now ( ) + 10 * 60 * 1000 ) ,
@@ -44,6 +45,39 @@ beforeEach(() => {
4445 prisma . $transaction . mockResolvedValue ( [ ] as any ) ;
4546} ) ;
4647
48+ // ---------------------------------------------------------------------------
49+ // generateAndStoreAuthCode
50+ // ---------------------------------------------------------------------------
51+
52+ describe ( 'generateAndStoreAuthCode' , ( ) => {
53+ test ( 'stores the granted scope with the authorization code' , async ( ) => {
54+ prisma . oAuthAuthorizationCode . create . mockResolvedValue ( VALID_AUTH_CODE ) ;
55+
56+ const code = await generateAndStoreAuthCode ( {
57+ clientId : 'test-client-id' ,
58+ userId : MOCK_USER_WITH_ACCOUNTS . id ,
59+ redirectUri : 'http://localhost:9999/callback' ,
60+ codeChallenge : 'challenge' ,
61+ scope : 'mcp other' ,
62+ resource : 'https://sourcebot.test/api/mcp' ,
63+ dpopJkt : 'dpop-thumbprint' ,
64+ } ) ;
65+
66+ expect ( code ) . toEqual ( expect . any ( String ) ) ;
67+ expect ( prisma . oAuthAuthorizationCode . create ) . toHaveBeenCalledWith ( {
68+ data : expect . objectContaining ( {
69+ clientId : 'test-client-id' ,
70+ userId : MOCK_USER_WITH_ACCOUNTS . id ,
71+ redirectUri : 'http://localhost:9999/callback' ,
72+ codeChallenge : 'challenge' ,
73+ scope : 'mcp other' ,
74+ resource : 'https://sourcebot.test/api/mcp' ,
75+ dpopJkt : 'dpop-thumbprint' ,
76+ } ) ,
77+ } ) ;
78+ } ) ;
79+ } ) ;
80+
4781// ---------------------------------------------------------------------------
4882// verifyAndExchangeCode
4983// ---------------------------------------------------------------------------
@@ -78,6 +112,33 @@ describe('verifyAndExchangeCode', () => {
78112 } ) ;
79113 } ) ;
80114
115+ test ( 'uses the scope bound to the authorization code when issuing tokens' , async ( ) => {
116+ prisma . oAuthAuthorizationCode . findUnique . mockResolvedValue ( {
117+ ...VALID_AUTH_CODE ,
118+ scope : 'mcp other' ,
119+ } ) ;
120+ prisma . oAuthAuthorizationCode . delete . mockResolvedValue ( VALID_AUTH_CODE ) ;
121+ prisma . oAuthToken . create . mockResolvedValue ( { } as never ) ;
122+ prisma . oAuthRefreshToken . create . mockResolvedValue ( { } as never ) ;
123+
124+ const result = await verifyAndExchangeCode ( {
125+ rawCode : VALID_CODE_HASH ,
126+ clientId : 'test-client-id' ,
127+ redirectUri : 'http://localhost:9999/callback' ,
128+ codeVerifier : 'myverifier' ,
129+ resource : null ,
130+ dpopJkt : null ,
131+ } ) ;
132+
133+ expect ( result ) . toMatchObject ( { scope : 'mcp other' } ) ;
134+ expect ( prisma . oAuthToken . create ) . toHaveBeenCalledWith ( {
135+ data : expect . objectContaining ( { scope : 'mcp other' } ) ,
136+ } ) ;
137+ expect ( prisma . oAuthRefreshToken . create ) . toHaveBeenCalledWith ( {
138+ data : expect . objectContaining ( { scope : 'mcp other' } ) ,
139+ } ) ;
140+ } ) ;
141+
81142 test ( 'returns invalid_grant if code is not found' , async ( ) => {
82143 prisma . oAuthAuthorizationCode . findUnique . mockResolvedValue ( null ) ;
83144
0 commit comments