Skip to content

Commit f26895a

Browse files
committed
test(mcp): unit-test surfaceOauthError typed and fallback paths
1 parent ccfa60f commit f26895a

2 files changed

Lines changed: 46 additions & 2 deletions

File tree

apps/sim/app/api/mcp/oauth/start/route.test.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ vi.mock('@/lib/auth/hybrid', () => hybridAuthMock)
3535
vi.mock('@/lib/workspaces/permissions/utils', () => permissionsMock)
3636
vi.mock('@/lib/mcp/oauth', () => mcpOauthMock)
3737

38-
import { GET } from './route'
38+
import { GET, surfaceOauthError } from './route'
3939

4040
describe('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+
})

apps/sim/app/api/mcp/oauth/start/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const logger = createLogger('McpOauthStartAPI')
2626
const OAUTH_START_TTL_MS = 10 * 60 * 1000
2727
const MAX_SURFACED_ERROR_LENGTH = 250
2828

29-
function surfaceOauthError(error: unknown): string {
29+
export function surfaceOauthError(error: unknown): string {
3030
// Spec-compliant OAuth servers throw typed subclasses with clean RFC 6749 fields.
3131
if (error instanceof OAuthError && !(error instanceof ServerError)) {
3232
return truncate(`${error.errorCode}: ${error.message}`)

0 commit comments

Comments
 (0)