@@ -35,7 +35,7 @@ vi.mock('@/lib/auth/hybrid', () => hybridAuthMock)
3535vi . mock ( '@/lib/workspaces/permissions/utils' , ( ) => permissionsMock )
3636vi . mock ( '@/lib/mcp/oauth' , ( ) => mcpOauthMock )
3737
38- import { GET } from './route'
38+ import { GET , surfaceOauthError } from './route'
3939
4040describe ( 'MCP OAuth start route' , ( ) => {
4141 beforeEach ( ( ) => {
@@ -135,3 +135,47 @@ describe('MCP OAuth start route', () => {
135135 expect ( mockMcpAuth ) . not . toHaveBeenCalled ( )
136136 } )
137137} )
138+
139+ describe ( 'surfaceOauthError' , ( ) => {
140+ it ( 'uses typed OAuthError errorCode and message for spec-compliant errors' , async ( ) => {
141+ const { InvalidGrantError } = await import ( '@modelcontextprotocol/sdk/server/auth/errors.js' )
142+ const err = new InvalidGrantError ( 'Refresh token expired' )
143+ expect ( surfaceOauthError ( err ) ) . toBe ( 'invalid_grant: Refresh token expired' )
144+ } )
145+
146+ it ( 'parses Raw body envelope for ServerError fallbacks (non-spec vendors)' , async ( ) => {
147+ const { ServerError } = await import ( '@modelcontextprotocol/sdk/server/auth/errors.js' )
148+ const err = new ServerError (
149+ 'HTTP 400: Invalid OAuth error response: zod error. Raw body: {"code":400,"message":"redirect URI https://example.com/cb is not allowed","retryable":false}'
150+ )
151+ expect ( surfaceOauthError ( err ) ) . toBe (
152+ 'Authorization server: redirect URI https://example.com/cb is not allowed'
153+ )
154+ } )
155+
156+ it ( 'prefers error_description over message over error in fallback envelope' , async ( ) => {
157+ const { ServerError } = await import ( '@modelcontextprotocol/sdk/server/auth/errors.js' )
158+ const err = new ServerError (
159+ 'HTTP 400: Invalid OAuth error response: zod. Raw body: {"error":"invalid_grant","error_description":"the description","message":"the message"}'
160+ )
161+ expect ( surfaceOauthError ( err ) ) . toBe ( 'Authorization server: the description' )
162+ } )
163+
164+ it ( 'returns first line of generic errors' , ( ) => {
165+ const err = new Error ( 'Network blip\n at fetch (...)' )
166+ expect ( surfaceOauthError ( err ) ) . toBe ( 'Network blip' )
167+ } )
168+
169+ it ( 'truncates messages longer than 250 chars with ellipsis' , async ( ) => {
170+ const { InvalidGrantError } = await import ( '@modelcontextprotocol/sdk/server/auth/errors.js' )
171+ const longMessage = 'x' . repeat ( 300 )
172+ const result = surfaceOauthError ( new InvalidGrantError ( longMessage ) )
173+ expect ( result . endsWith ( '…' ) ) . toBe ( true )
174+ expect ( result . length ) . toBe ( 251 ) // 250 chars + ellipsis
175+ } )
176+
177+ it ( 'returns generic fallback for non-Error values' , ( ) => {
178+ expect ( surfaceOauthError ( null ) ) . toBe ( 'Failed to start OAuth flow' )
179+ expect ( surfaceOauthError ( undefined ) ) . toBe ( 'Failed to start OAuth flow' )
180+ } )
181+ } )
0 commit comments