mcporter/tests/runtime-oauth-detection.test.ts
yukaibo.me cc06993494 fix: detect auth errors from error.code property (StreamableHTTPError/SseError)
MCP SDK's StreamableHTTPError/SseError store the HTTP status code in
error.code, but the message text may not contain the numeric status.
analyzeConnectionError now reads error.code (100-599) before falling
back to message-text parsing, so OAuth promotion triggers correctly
for 401 responses on Streamable HTTP transport.
2026-03-28 19:21:23 +00:00

78 lines
2.6 KiB
TypeScript

import { UnauthorizedError } from '@modelcontextprotocol/sdk/client/auth.js';
import { describe, expect, it, vi } from 'vitest';
import type { ServerDefinition } from '../src/config.js';
import { isUnauthorizedError, maybeEnableOAuth } from '../src/runtime-oauth-support.js';
const logger = {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
};
describe('maybeEnableOAuth', () => {
const baseDefinition: ServerDefinition = {
name: 'adhoc-server',
command: { kind: 'http', url: new URL('https://example.com/mcp') },
source: { kind: 'local', path: '<adhoc>' },
};
it('returns an updated definition for ad-hoc HTTP servers', () => {
const updated = maybeEnableOAuth(baseDefinition, logger as never);
expect(updated).toBeDefined();
expect(updated?.auth).toBe('oauth');
expect(updated?.tokenCacheDir).toBeUndefined();
expect(logger.info).toHaveBeenCalled();
});
it('enables OAuth for non-ad-hoc HTTP servers (issue #38)', () => {
const def: ServerDefinition = {
name: 'local-server',
command: { kind: 'http', url: new URL('https://example.com') },
source: { kind: 'local', path: '/tmp/config.json' },
};
const updated = maybeEnableOAuth(def, logger as never);
expect(updated).toBeDefined();
expect(updated?.auth).toBe('oauth');
});
it('does not mutate stdio servers', () => {
const def: ServerDefinition = {
name: 'stdio-server',
command: { kind: 'stdio', command: 'echo', args: [], cwd: process.cwd() },
};
const updated = maybeEnableOAuth(def, logger as never);
expect(updated).toBeUndefined();
});
it('does not re-promote servers already configured for oauth', () => {
const def: ServerDefinition = {
name: 'oauth-server',
auth: 'oauth',
command: { kind: 'http', url: new URL('https://example.com') },
};
const updated = maybeEnableOAuth(def, logger as never);
expect(updated).toBeUndefined();
});
});
describe('isUnauthorizedError helper', () => {
it('matches UnauthorizedError instances', () => {
const err = new UnauthorizedError('Unauthorized');
expect(isUnauthorizedError(err)).toBe(true);
});
it('matches generic errors with 401 codes', () => {
expect(isUnauthorizedError(new Error('SSE error: Non-200 status code (401)'))).toBe(true);
});
it('ignores unrelated errors', () => {
expect(isUnauthorizedError(new Error('network timeout'))).toBe(false);
});
it('matches errors with code=401 even when message lacks 401', () => {
const err = Object.assign(new Error('Error POSTing to endpoint: {}'), { code: 401 });
expect(isUnauthorizedError(err)).toBe(true);
});
});