2025-08-13 09:45:06 -06:00
|
|
|
import { logger } from '@librechat/data-schemas';
|
2025-08-16 20:45:55 -04:00
|
|
|
import type { TokenMethods } from '@librechat/data-schemas';
|
2025-08-13 09:45:06 -06:00
|
|
|
import type { TUser } from 'librechat-data-provider';
|
|
|
|
|
import type { FlowStateManager } from '~/flow/manager';
|
|
|
|
|
import type { MCPOAuthTokens } from '~/mcp/oauth';
|
2025-08-16 20:45:55 -04:00
|
|
|
import type * as t from '~/mcp/types';
|
|
|
|
|
import { MCPConnectionFactory } from '~/mcp/MCPConnectionFactory';
|
|
|
|
|
import { MCPConnection } from '~/mcp/connection';
|
2025-08-13 09:45:06 -06:00
|
|
|
import { MCPOAuthHandler } from '~/mcp/oauth';
|
|
|
|
|
import { processMCPEnv } from '~/utils';
|
|
|
|
|
|
2025-08-16 20:45:55 -04:00
|
|
|
jest.mock('~/mcp/connection');
|
2025-08-13 09:45:06 -06:00
|
|
|
jest.mock('~/mcp/oauth');
|
|
|
|
|
jest.mock('~/utils');
|
|
|
|
|
jest.mock('@librechat/data-schemas', () => ({
|
|
|
|
|
logger: {
|
|
|
|
|
info: jest.fn(),
|
|
|
|
|
warn: jest.fn(),
|
|
|
|
|
error: jest.fn(),
|
|
|
|
|
debug: jest.fn(),
|
|
|
|
|
},
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
const mockLogger = logger as jest.Mocked<typeof logger>;
|
|
|
|
|
const mockProcessMCPEnv = processMCPEnv as jest.MockedFunction<typeof processMCPEnv>;
|
|
|
|
|
const mockMCPConnection = MCPConnection as jest.MockedClass<typeof MCPConnection>;
|
|
|
|
|
const mockMCPOAuthHandler = MCPOAuthHandler as jest.Mocked<typeof MCPOAuthHandler>;
|
|
|
|
|
|
|
|
|
|
describe('MCPConnectionFactory', () => {
|
|
|
|
|
let mockUser: TUser;
|
|
|
|
|
let mockServerConfig: t.MCPOptions;
|
|
|
|
|
let mockFlowManager: jest.Mocked<FlowStateManager<MCPOAuthTokens | null>>;
|
|
|
|
|
let mockConnectionInstance: jest.Mocked<MCPConnection>;
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
jest.clearAllMocks();
|
|
|
|
|
mockUser = {
|
|
|
|
|
id: 'user123',
|
|
|
|
|
email: 'test@example.com',
|
|
|
|
|
} as TUser;
|
|
|
|
|
|
|
|
|
|
mockServerConfig = {
|
|
|
|
|
command: 'node',
|
|
|
|
|
args: ['server.js'],
|
|
|
|
|
initTimeout: 5000,
|
|
|
|
|
} as t.MCPOptions;
|
|
|
|
|
|
|
|
|
|
mockFlowManager = {
|
|
|
|
|
createFlow: jest.fn(),
|
|
|
|
|
createFlowWithHandler: jest.fn(),
|
|
|
|
|
getFlowState: jest.fn(),
|
⚠️ fix: OAuth Error and Token Expiry Detection and Reporting Improvements (#10922)
* fix: create new flows on invalid_grant errors
* chore: fix failing test
* chore: keep isOAuthError test function in sync with implementation
* test: add tests for OAuth error detection on invalid grant errors
* test: add tests for creating new flows when token expires
* test: add test for flow clean up prior to creation
* refactor: consolidate token expiration handling in FlowStateManager
- Removed the old token expiration checks and replaced them with a new method, `isTokenExpired`, to streamline the logic.
- Introduced `normalizeExpirationTimestamp` to handle timestamp normalization for both seconds and milliseconds.
- Updated tests to ensure proper functionality of flow management with token expiration scenarios.
* fix: conditionally setup cleanup handlers in FlowStateManager
- Updated the FlowStateManager constructor to only call setupCleanupHandlers if the ci parameter is not set, improving flexibility in flow management.
* chore: enhance OAuth token refresh logging
- Introduced a new method, `processRefreshResponse`, to streamline the processing of token refresh responses from the OAuth server.
- Improved logging to provide detailed information about token refresh operations, including whether new tokens were received and if the refresh token was rotated.
- Updated existing token handling logic to utilize the new method, ensuring consistency and clarity in token management.
* chore: enhance logging for MCP server reinitialization
- Updated the logging in the reinitMCPServer function to provide more detailed information about the response, including success status, OAuth requirements, presence of the OAuth URL, and the count of tools involved. This improves the clarity and usefulness of logs for debugging purposes.
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
2025-12-12 10:51:28 -08:00
|
|
|
deleteFlow: jest.fn().mockResolvedValue(true),
|
2025-08-13 09:45:06 -06:00
|
|
|
} as unknown as jest.Mocked<FlowStateManager<MCPOAuthTokens | null>>;
|
|
|
|
|
|
|
|
|
|
mockConnectionInstance = {
|
|
|
|
|
connect: jest.fn(),
|
|
|
|
|
isConnected: jest.fn(),
|
|
|
|
|
setOAuthTokens: jest.fn(),
|
|
|
|
|
on: jest.fn().mockReturnValue(mockConnectionInstance),
|
2025-09-11 18:54:43 -04:00
|
|
|
once: jest.fn().mockReturnValue(mockConnectionInstance),
|
|
|
|
|
off: jest.fn().mockReturnValue(mockConnectionInstance),
|
|
|
|
|
removeListener: jest.fn().mockReturnValue(mockConnectionInstance),
|
2025-08-13 09:45:06 -06:00
|
|
|
emit: jest.fn(),
|
|
|
|
|
} as unknown as jest.Mocked<MCPConnection>;
|
|
|
|
|
|
|
|
|
|
mockMCPConnection.mockImplementation(() => mockConnectionInstance);
|
|
|
|
|
mockProcessMCPEnv.mockReturnValue(mockServerConfig);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('static create method', () => {
|
|
|
|
|
it('should create a basic connection without OAuth', async () => {
|
|
|
|
|
const basicOptions = {
|
|
|
|
|
serverName: 'test-server',
|
|
|
|
|
serverConfig: mockServerConfig,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
mockConnectionInstance.isConnected.mockResolvedValue(true);
|
|
|
|
|
|
|
|
|
|
const connection = await MCPConnectionFactory.create(basicOptions);
|
|
|
|
|
|
|
|
|
|
expect(connection).toBe(mockConnectionInstance);
|
2025-08-16 20:45:55 -04:00
|
|
|
expect(mockProcessMCPEnv).toHaveBeenCalledWith({ options: mockServerConfig });
|
2025-08-13 09:45:06 -06:00
|
|
|
expect(mockMCPConnection).toHaveBeenCalledWith({
|
|
|
|
|
serverName: 'test-server',
|
|
|
|
|
serverConfig: mockServerConfig,
|
|
|
|
|
userId: undefined,
|
|
|
|
|
oauthTokens: null,
|
|
|
|
|
});
|
|
|
|
|
expect(mockConnectionInstance.connect).toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should create a connection with OAuth', async () => {
|
|
|
|
|
const basicOptions = {
|
|
|
|
|
serverName: 'test-server',
|
|
|
|
|
serverConfig: mockServerConfig,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const oauthOptions = {
|
|
|
|
|
useOAuth: true as const,
|
|
|
|
|
user: mockUser,
|
|
|
|
|
flowManager: mockFlowManager,
|
|
|
|
|
tokenMethods: {
|
|
|
|
|
findToken: jest.fn(),
|
|
|
|
|
createToken: jest.fn(),
|
|
|
|
|
updateToken: jest.fn(),
|
|
|
|
|
deleteTokens: jest.fn(),
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const mockTokens: MCPOAuthTokens = {
|
|
|
|
|
access_token: 'access123',
|
|
|
|
|
refresh_token: 'refresh123',
|
|
|
|
|
token_type: 'Bearer',
|
|
|
|
|
obtained_at: Date.now(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
mockFlowManager.createFlowWithHandler.mockResolvedValue(mockTokens);
|
|
|
|
|
mockConnectionInstance.isConnected.mockResolvedValue(true);
|
|
|
|
|
|
|
|
|
|
const connection = await MCPConnectionFactory.create(basicOptions, oauthOptions);
|
|
|
|
|
|
|
|
|
|
expect(connection).toBe(mockConnectionInstance);
|
2025-08-16 20:45:55 -04:00
|
|
|
expect(mockProcessMCPEnv).toHaveBeenCalledWith({ options: mockServerConfig, user: mockUser });
|
2025-08-13 09:45:06 -06:00
|
|
|
expect(mockMCPConnection).toHaveBeenCalledWith({
|
|
|
|
|
serverName: 'test-server',
|
|
|
|
|
serverConfig: mockServerConfig,
|
|
|
|
|
userId: 'user123',
|
|
|
|
|
oauthTokens: mockTokens,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('OAuth token handling', () => {
|
|
|
|
|
it('should return null when no findToken method is provided', async () => {
|
|
|
|
|
const basicOptions = {
|
|
|
|
|
serverName: 'test-server',
|
|
|
|
|
serverConfig: mockServerConfig,
|
|
|
|
|
};
|
|
|
|
|
|
2025-08-16 20:45:55 -04:00
|
|
|
const oauthOptions: t.OAuthConnectionOptions = {
|
2025-08-13 09:45:06 -06:00
|
|
|
useOAuth: true as const,
|
|
|
|
|
user: mockUser,
|
|
|
|
|
flowManager: mockFlowManager,
|
|
|
|
|
tokenMethods: {
|
2025-08-16 20:45:55 -04:00
|
|
|
findToken: undefined as unknown as TokenMethods['findToken'],
|
2025-08-13 09:45:06 -06:00
|
|
|
createToken: jest.fn(),
|
|
|
|
|
updateToken: jest.fn(),
|
|
|
|
|
deleteTokens: jest.fn(),
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
mockConnectionInstance.isConnected.mockResolvedValue(true);
|
|
|
|
|
|
|
|
|
|
await MCPConnectionFactory.create(basicOptions, oauthOptions);
|
|
|
|
|
|
|
|
|
|
expect(mockFlowManager.createFlowWithHandler).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should handle token retrieval errors gracefully', async () => {
|
|
|
|
|
const basicOptions = {
|
|
|
|
|
serverName: 'test-server',
|
|
|
|
|
serverConfig: mockServerConfig,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const oauthOptions = {
|
|
|
|
|
useOAuth: true as const,
|
|
|
|
|
user: mockUser,
|
|
|
|
|
flowManager: mockFlowManager,
|
|
|
|
|
tokenMethods: {
|
|
|
|
|
findToken: jest.fn(),
|
|
|
|
|
createToken: jest.fn(),
|
|
|
|
|
updateToken: jest.fn(),
|
|
|
|
|
deleteTokens: jest.fn(),
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
mockFlowManager.createFlowWithHandler.mockRejectedValue(new Error('Token fetch failed'));
|
|
|
|
|
mockConnectionInstance.isConnected.mockResolvedValue(true);
|
|
|
|
|
|
|
|
|
|
const connection = await MCPConnectionFactory.create(basicOptions, oauthOptions);
|
|
|
|
|
|
|
|
|
|
expect(connection).toBe(mockConnectionInstance);
|
|
|
|
|
expect(mockMCPConnection).toHaveBeenCalledWith({
|
|
|
|
|
serverName: 'test-server',
|
|
|
|
|
serverConfig: mockServerConfig,
|
|
|
|
|
userId: 'user123',
|
|
|
|
|
oauthTokens: null,
|
|
|
|
|
});
|
|
|
|
|
expect(mockLogger.debug).toHaveBeenCalledWith(
|
|
|
|
|
expect.stringContaining('No existing tokens found or error loading tokens'),
|
|
|
|
|
expect.any(Error),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('OAuth event handling', () => {
|
|
|
|
|
it('should handle oauthRequired event for returnOnOAuth scenario', async () => {
|
|
|
|
|
const basicOptions = {
|
|
|
|
|
serverName: 'test-server',
|
|
|
|
|
serverConfig: {
|
|
|
|
|
...mockServerConfig,
|
|
|
|
|
url: 'https://api.example.com',
|
|
|
|
|
type: 'sse' as const,
|
|
|
|
|
} as t.SSEOptions,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const oauthOptions = {
|
|
|
|
|
useOAuth: true as const,
|
|
|
|
|
user: mockUser,
|
|
|
|
|
flowManager: mockFlowManager,
|
|
|
|
|
returnOnOAuth: true,
|
|
|
|
|
oauthStart: jest.fn(),
|
|
|
|
|
tokenMethods: {
|
|
|
|
|
findToken: jest.fn(),
|
|
|
|
|
createToken: jest.fn(),
|
|
|
|
|
updateToken: jest.fn(),
|
|
|
|
|
deleteTokens: jest.fn(),
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const mockFlowData = {
|
|
|
|
|
authorizationUrl: 'https://auth.example.com',
|
|
|
|
|
flowId: 'flow123',
|
|
|
|
|
flowMetadata: {
|
|
|
|
|
serverName: 'test-server',
|
|
|
|
|
userId: 'user123',
|
|
|
|
|
serverUrl: 'https://api.example.com',
|
|
|
|
|
state: 'random-state',
|
|
|
|
|
clientInfo: { client_id: 'client123' },
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
mockMCPOAuthHandler.initiateOAuthFlow.mockResolvedValue(mockFlowData);
|
|
|
|
|
mockFlowManager.createFlow.mockRejectedValue(new Error('Timeout expected'));
|
|
|
|
|
mockConnectionInstance.isConnected.mockResolvedValue(false);
|
|
|
|
|
|
|
|
|
|
let oauthRequiredHandler: (data: Record<string, unknown>) => Promise<void>;
|
|
|
|
|
mockConnectionInstance.on.mockImplementation((event, handler) => {
|
|
|
|
|
if (event === 'oauthRequired') {
|
|
|
|
|
oauthRequiredHandler = handler as (data: Record<string, unknown>) => Promise<void>;
|
|
|
|
|
}
|
|
|
|
|
return mockConnectionInstance;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await MCPConnectionFactory.create(basicOptions, oauthOptions);
|
|
|
|
|
} catch {
|
|
|
|
|
// Expected to fail due to connection not established
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
expect(oauthRequiredHandler!).toBeDefined();
|
|
|
|
|
|
|
|
|
|
await oauthRequiredHandler!({ serverUrl: 'https://api.example.com' });
|
|
|
|
|
|
|
|
|
|
expect(mockMCPOAuthHandler.initiateOAuthFlow).toHaveBeenCalledWith(
|
|
|
|
|
'test-server',
|
|
|
|
|
'https://api.example.com',
|
|
|
|
|
'user123',
|
2025-10-11 17:17:12 +02:00
|
|
|
{},
|
2025-08-13 09:45:06 -06:00
|
|
|
undefined,
|
|
|
|
|
);
|
|
|
|
|
expect(oauthOptions.oauthStart).toHaveBeenCalledWith('https://auth.example.com');
|
|
|
|
|
expect(mockConnectionInstance.emit).toHaveBeenCalledWith(
|
|
|
|
|
'oauthFailed',
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
message: 'OAuth flow initiated - return early',
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
});
|
⚠️ fix: OAuth Error and Token Expiry Detection and Reporting Improvements (#10922)
* fix: create new flows on invalid_grant errors
* chore: fix failing test
* chore: keep isOAuthError test function in sync with implementation
* test: add tests for OAuth error detection on invalid grant errors
* test: add tests for creating new flows when token expires
* test: add test for flow clean up prior to creation
* refactor: consolidate token expiration handling in FlowStateManager
- Removed the old token expiration checks and replaced them with a new method, `isTokenExpired`, to streamline the logic.
- Introduced `normalizeExpirationTimestamp` to handle timestamp normalization for both seconds and milliseconds.
- Updated tests to ensure proper functionality of flow management with token expiration scenarios.
* fix: conditionally setup cleanup handlers in FlowStateManager
- Updated the FlowStateManager constructor to only call setupCleanupHandlers if the ci parameter is not set, improving flexibility in flow management.
* chore: enhance OAuth token refresh logging
- Introduced a new method, `processRefreshResponse`, to streamline the processing of token refresh responses from the OAuth server.
- Improved logging to provide detailed information about token refresh operations, including whether new tokens were received and if the refresh token was rotated.
- Updated existing token handling logic to utilize the new method, ensuring consistency and clarity in token management.
* chore: enhance logging for MCP server reinitialization
- Updated the logging in the reinitMCPServer function to provide more detailed information about the response, including success status, OAuth requirements, presence of the OAuth URL, and the count of tools involved. This improves the clarity and usefulness of logs for debugging purposes.
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
2025-12-12 10:51:28 -08:00
|
|
|
|
|
|
|
|
it('should delete existing flow before creating new OAuth flow to prevent stale codeVerifier', async () => {
|
|
|
|
|
const basicOptions = {
|
|
|
|
|
serverName: 'test-server',
|
|
|
|
|
serverConfig: mockServerConfig,
|
|
|
|
|
user: mockUser,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const oauthOptions = {
|
|
|
|
|
user: mockUser,
|
|
|
|
|
useOAuth: true,
|
|
|
|
|
returnOnOAuth: true,
|
|
|
|
|
oauthStart: jest.fn(),
|
|
|
|
|
flowManager: mockFlowManager,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const mockFlowData = {
|
|
|
|
|
authorizationUrl: 'https://auth.example.com',
|
|
|
|
|
flowId: 'user123:test-server',
|
|
|
|
|
flowMetadata: {
|
|
|
|
|
serverName: 'test-server',
|
|
|
|
|
userId: 'user123',
|
|
|
|
|
serverUrl: 'https://api.example.com',
|
|
|
|
|
state: 'test-state',
|
|
|
|
|
codeVerifier: 'new-code-verifier-xyz',
|
|
|
|
|
clientInfo: { client_id: 'test-client' },
|
|
|
|
|
metadata: {
|
|
|
|
|
authorization_endpoint: 'https://auth.example.com/authorize',
|
|
|
|
|
token_endpoint: 'https://auth.example.com/token',
|
|
|
|
|
issuer: 'https://api.example.com',
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
mockMCPOAuthHandler.initiateOAuthFlow.mockResolvedValue(mockFlowData);
|
|
|
|
|
mockFlowManager.deleteFlow.mockResolvedValue(true);
|
|
|
|
|
mockFlowManager.createFlow.mockRejectedValue(new Error('Timeout expected'));
|
|
|
|
|
mockConnectionInstance.isConnected.mockResolvedValue(false);
|
|
|
|
|
|
|
|
|
|
let oauthRequiredHandler: (data: Record<string, unknown>) => Promise<void>;
|
|
|
|
|
mockConnectionInstance.on.mockImplementation((event, handler) => {
|
|
|
|
|
if (event === 'oauthRequired') {
|
|
|
|
|
oauthRequiredHandler = handler as (data: Record<string, unknown>) => Promise<void>;
|
|
|
|
|
}
|
|
|
|
|
return mockConnectionInstance;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await MCPConnectionFactory.create(basicOptions, oauthOptions);
|
|
|
|
|
} catch {
|
|
|
|
|
// Expected to fail due to connection not established
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await oauthRequiredHandler!({ serverUrl: 'https://api.example.com' });
|
|
|
|
|
|
|
|
|
|
// Verify deleteFlow was called with correct parameters
|
|
|
|
|
expect(mockFlowManager.deleteFlow).toHaveBeenCalledWith('user123:test-server', 'mcp_oauth');
|
|
|
|
|
|
|
|
|
|
// Verify deleteFlow was called before createFlow
|
|
|
|
|
const deleteCallOrder = mockFlowManager.deleteFlow.mock.invocationCallOrder[0];
|
|
|
|
|
const createCallOrder = mockFlowManager.createFlow.mock.invocationCallOrder[0];
|
|
|
|
|
expect(deleteCallOrder).toBeLessThan(createCallOrder);
|
|
|
|
|
|
|
|
|
|
// Verify createFlow was called with fresh metadata
|
|
|
|
|
expect(mockFlowManager.createFlow).toHaveBeenCalledWith(
|
|
|
|
|
'user123:test-server',
|
|
|
|
|
'mcp_oauth',
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
codeVerifier: 'new-code-verifier-xyz',
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
});
|
2025-08-13 09:45:06 -06:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('connection retry logic', () => {
|
|
|
|
|
it('should establish connection successfully', async () => {
|
|
|
|
|
const basicOptions = {
|
|
|
|
|
serverName: 'test-server',
|
|
|
|
|
serverConfig: mockServerConfig, // Use default 5000ms timeout
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
mockConnectionInstance.connect.mockResolvedValue(undefined);
|
|
|
|
|
mockConnectionInstance.isConnected.mockResolvedValue(true);
|
|
|
|
|
|
|
|
|
|
const connection = await MCPConnectionFactory.create(basicOptions);
|
|
|
|
|
|
|
|
|
|
expect(connection).toBe(mockConnectionInstance);
|
|
|
|
|
expect(mockConnectionInstance.connect).toHaveBeenCalledTimes(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should handle OAuth errors during connection attempts', async () => {
|
|
|
|
|
const basicOptions = {
|
|
|
|
|
serverName: 'test-server',
|
|
|
|
|
serverConfig: mockServerConfig,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const oauthOptions = {
|
|
|
|
|
useOAuth: true as const,
|
|
|
|
|
user: mockUser,
|
|
|
|
|
flowManager: mockFlowManager,
|
|
|
|
|
oauthStart: jest.fn(),
|
|
|
|
|
tokenMethods: {
|
|
|
|
|
findToken: jest.fn(),
|
|
|
|
|
createToken: jest.fn(),
|
|
|
|
|
updateToken: jest.fn(),
|
|
|
|
|
deleteTokens: jest.fn(),
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const oauthError = new Error('Non-200 status code (401)');
|
|
|
|
|
(oauthError as unknown as Record<string, unknown>).isOAuthError = true;
|
|
|
|
|
|
|
|
|
|
mockConnectionInstance.connect.mockRejectedValue(oauthError);
|
|
|
|
|
mockConnectionInstance.isConnected.mockResolvedValue(false);
|
|
|
|
|
|
|
|
|
|
await expect(MCPConnectionFactory.create(basicOptions, oauthOptions)).rejects.toThrow(
|
|
|
|
|
'Non-200 status code (401)',
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(mockLogger.info).toHaveBeenCalledWith(
|
|
|
|
|
expect.stringContaining('OAuth required, stopping connection attempts'),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('isOAuthError method', () => {
|
|
|
|
|
it('should identify OAuth errors by message content', async () => {
|
|
|
|
|
const basicOptions = {
|
|
|
|
|
serverName: 'test-server',
|
|
|
|
|
serverConfig: mockServerConfig,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const oauthOptions = {
|
|
|
|
|
useOAuth: true as const,
|
|
|
|
|
user: mockUser,
|
|
|
|
|
flowManager: mockFlowManager,
|
|
|
|
|
tokenMethods: {
|
|
|
|
|
findToken: jest.fn(),
|
|
|
|
|
createToken: jest.fn(),
|
|
|
|
|
updateToken: jest.fn(),
|
|
|
|
|
deleteTokens: jest.fn(),
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const error401 = new Error('401 Unauthorized');
|
|
|
|
|
|
|
|
|
|
mockConnectionInstance.connect.mockRejectedValue(error401);
|
|
|
|
|
mockConnectionInstance.isConnected.mockResolvedValue(false);
|
|
|
|
|
|
|
|
|
|
await expect(MCPConnectionFactory.create(basicOptions, oauthOptions)).rejects.toThrow('401');
|
|
|
|
|
expect(mockLogger.info).toHaveBeenCalledWith(
|
|
|
|
|
expect.stringContaining('OAuth required, stopping connection attempts'),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|