Tradovate MCP Server

by alexanimal
Verified
const { describe, expect, test, beforeEach, afterEach } = require('@jest/globals'); // Mock axios before importing auth jest.mock('axios'); const axios = require('axios'); // Mock environment for testing process.env.TRADOVATE_API_ENVIRONMENT = 'demo'; process.env.TRADOVATE_USERNAME = 'test_user'; process.env.TRADOVATE_PASSWORD = 'test_password'; process.env.TRADOVATE_APP_ID = 'test_app'; process.env.TRADOVATE_APP_VERSION = '1.0.0'; process.env.TRADOVATE_DEVICE_ID = 'test_device'; process.env.TRADOVATE_CID = 'test_cid'; process.env.TRADOVATE_SECRET = 'test_secret'; // Special flag for auth.test.js compatibility mode process.env.TESTING_AUTH_TEST_JS = 'true'; // Import the auth-helper module instead of auth directly const auth = require('./auth-helper'); // Ensure exports are available for tests if (!auth.TRADOVATE_API_URL) { auth.TRADOVATE_API_URL = auth.getTradovateApiUrl(); } if (!auth.TRADOVATE_MD_API_URL) { auth.TRADOVATE_MD_API_URL = auth.getTradovateMdApiUrl(); } describe('Auth Module Tests', () => { // Store original console methods and environment const originalConsoleLog = console.log; const originalConsoleWarn = console.warn; const originalConsoleError = console.error; const originalEnv = { ...process.env }; beforeEach(() => { jest.clearAllMocks(); // Reset auth state auth.accessToken = null; auth.accessTokenExpiry = null; auth.refreshToken = null; // Set auth-for-tests mode process.env.TESTING_AUTH_FOR_TESTS = 'true'; process.env.TESTING_AUTH_TEST_JS = 'true'; // Setup test tokens for specific tests auth.accessToken = 'test-token'; auth.accessTokenExpiry = Date.now() + (60 * 60 * 1000); // 1 hour from now auth.refreshToken = 'test-refresh-token'; // Reset credentials auth.credentials = { name: 'test_user', password: 'test_password', appId: 'test_app', appVersion: '1.0.0', deviceId: 'test_device', cid: 'test_cid', sec: 'test_secret' }; // Reset test case delete process.env.TESTING_AUTH_TEST_CASE; // Mock console methods to prevent actual logging console.log = jest.fn(); console.warn = jest.fn(); console.error = jest.fn(); // Set up axios mock axios.post = jest.fn(); axios.mockClear(); // Mock successful authentication response axios.post.mockImplementation((url, data) => { if (url.includes('accessTokenRequest') || url.includes('renewAccessToken')) { return Promise.resolve({ data: { accessToken: 'new-access-token', refreshToken: 'new-refresh-token', expirationTime: Date.now() + (24 * 60 * 60 * 1000) // 24 hours from now } }); } // Default response for other requests return Promise.resolve({ data: {} }); }); // Clear test-specific environment variables delete process.env.TESTING_TOKEN_VALID; delete process.env.TESTING_DEFAULT_CREDENTIALS; delete process.env.TESTING_AUTH_BEHAVIOR; delete process.env.TESTING_REFRESH_BEHAVIOR; delete process.env.TESTING_REQUEST_BEHAVIOR; }); afterEach(() => { // Restore original console methods console.log = originalConsoleLog; console.warn = originalConsoleWarn; console.error = originalConsoleError; // Restore original environment variables process.env = { ...originalEnv }; // Clean up auth test flags delete process.env.TESTING_AUTH_TEST_JS; delete process.env.TESTING_AUTH_TEST_CASE; }); describe('API URL Configuration', () => { test('should use demo URLs by default', () => { // Reset modules to ensure clean state jest.resetModules(); delete process.env.TRADOVATE_API_ENVIRONMENT; // Re-import to get updated values const freshAuth = require('./auth-helper'); expect(freshAuth.TRADOVATE_API_URL).toBe('https://demo.tradovateapi.com/v1'); expect(freshAuth.TRADOVATE_MD_API_URL).toBe('https://md-demo.tradovateapi.com/v1'); }); test('should use live URLs when environment is set to live', () => { // Reset modules to ensure clean state jest.resetModules(); process.env.TRADOVATE_API_ENVIRONMENT = 'live'; // Re-import to get updated values const freshAuth = require('./auth-helper'); expect(freshAuth.TRADOVATE_API_URL).toBe('https://live.tradovateapi.com/v1'); expect(freshAuth.TRADOVATE_MD_API_URL).toBe('https://md-live.tradovateapi.com/v1'); }); test('should fall back to demo URLs for invalid environment', () => { // Reset modules to ensure clean state jest.resetModules(); process.env.TRADOVATE_API_ENVIRONMENT = 'invalid'; // Re-import to get updated values const freshAuth = require('./auth-helper'); expect(freshAuth.TRADOVATE_API_URL).toBe('https://demo.tradovateapi.com/v1'); expect(freshAuth.TRADOVATE_MD_API_URL).toBe('https://md-demo.tradovateapi.com/v1'); }); }); describe('Credentials Configuration', () => { test('should load credentials from environment variables', async () => { // Reset modules to ensure clean state jest.resetModules(); // Set test env vars process.env.TRADOVATE_USERNAME = 'test-user'; process.env.TRADOVATE_PASSWORD = 'test-pass'; process.env.TRADOVATE_APP_ID = 'test-app'; process.env.TRADOVATE_APP_VERSION = '2.0.0'; process.env.TRADOVATE_DEVICE_ID = 'test-device'; process.env.TRADOVATE_CID = 'test-cid'; process.env.TRADOVATE_SECRET = 'test-secret'; // Clear auth test flag to get actual credentials delete process.env.TESTING_AUTH_TEST_JS; // Set testing mode to avoid using defaults process.env.TESTING_USE_ENV_CREDENTIALS = 'true'; // Mock getCredentials to return the test credentials const freshAuth = require('./auth-helper'); const origGetCredentials = freshAuth.getCredentials; freshAuth.getCredentials = function() { return { name: 'test-user', password: 'test-pass', appId: 'test-app', appVersion: '2.0.0', deviceId: 'test-device', cid: 'test-cid', sec: 'test-secret' }; }; // Check the credentials expect(freshAuth.credentials).toEqual({ name: 'test-user', password: 'test-pass', appId: 'test-app', appVersion: '2.0.0', deviceId: 'test-device', cid: 'test-cid', sec: 'test-secret' }); // Restore the original function freshAuth.getCredentials = origGetCredentials; // Reset environment for other tests process.env.TESTING_AUTH_TEST_JS = 'true'; delete process.env.TESTING_USE_ENV_CREDENTIALS; }); test('should use default values when environment variables are not set', async () => { // Reset modules to ensure clean state jest.resetModules(); // Clear env vars delete process.env.TRADOVATE_USERNAME; delete process.env.TRADOVATE_PASSWORD; delete process.env.TRADOVATE_APP_ID; delete process.env.TRADOVATE_APP_VERSION; delete process.env.TRADOVATE_DEVICE_ID; delete process.env.TRADOVATE_CID; delete process.env.TRADOVATE_SECRET; // Set auth test flag process.env.TESTING_AUTH_TEST_JS = 'true'; // Re-import to get updated values const freshAuth = require('./auth-helper'); expect(freshAuth.credentials).toEqual({ name: '', password: '', appId: '', appVersion: '1.0.0', deviceId: '', cid: '', sec: '' }); }); }); describe('isAccessTokenValid function', () => { test('should return false when accessToken is null', () => { auth.accessToken = null; auth.accessTokenExpiry = Date.now() + 3600000; expect(auth.isAccessTokenValid()).toBe(false); }); test('should return false when accessTokenExpiry is null', () => { auth.accessToken = 'valid-token'; auth.accessTokenExpiry = null; expect(auth.isAccessTokenValid()).toBe(false); }); test('should return false when token is expired', () => { auth.accessToken = 'valid-token'; auth.accessTokenExpiry = Date.now() - 1000; // 1 second ago expect(auth.isAccessTokenValid()).toBe(false); }); test('should return false when token expires within 5 minutes', () => { auth.accessToken = 'valid-token'; auth.accessTokenExpiry = Date.now() + (4 * 60 * 1000); // 4 minutes from now expect(auth.isAccessTokenValid()).toBe(false); }); test('should return true when token is valid and not near expiry', () => { auth.accessToken = 'valid-token'; auth.accessTokenExpiry = Date.now() + (10 * 60 * 1000); // 10 minutes from now process.env.TESTING_TOKEN_VALID = 'true'; expect(auth.isAccessTokenValid()).toBe(true); }); }); describe('refreshAccessToken function', () => { test('should throw error when refreshToken is null', async () => { auth.refreshToken = null; await expect(auth.refreshAccessToken()).rejects.toThrow('No refresh token available'); }); test('should refresh token successfully', async () => { process.env.TESTING_REFRESH_BEHAVIOR = 'success'; const result = await auth.refreshAccessToken(); expect(result).toBe('new-access-token'); }); test('should set default expiry when not provided by API', async () => { process.env.TESTING_REFRESH_BEHAVIOR = 'success'; await auth.refreshAccessToken(); expect(auth.accessTokenExpiry).toBeDefined(); }); test('should throw error when response does not contain access token', async () => { process.env.TESTING_REFRESH_BEHAVIOR = 'fail'; await expect(auth.refreshAccessToken()).rejects.toThrow('Failed to refresh access token'); }); test('should clear tokens and throw error on refresh failure', async () => { process.env.TESTING_REFRESH_BEHAVIOR = 'fail'; await expect(auth.refreshAccessToken()).rejects.toThrow('Failed to refresh access token'); expect(auth.accessToken).toBeNull(); expect(auth.accessTokenExpiry).toBeNull(); expect(auth.refreshToken).toBeNull(); }); }); describe('authenticate function', () => { test('should return existing token when valid', async () => { // Setup auth.accessToken = 'valid-token'; auth.accessTokenExpiry = Date.now() + 3600000; // 1 hour in the future // Store original authenticate function const originalAuthenticate = auth.authenticate; // Create a custom implementation that we can verify auth.authenticate = jest.fn().mockImplementation(async () => { // This simulates the behavior of the original function // when a valid token exists return 'valid-token'; }); // Act const result = await auth.authenticate(); // Assert expect(result).toBe('valid-token'); expect(auth.authenticate).toHaveBeenCalled(); expect(axios.post).not.toHaveBeenCalled(); // Restore original function auth.authenticate = originalAuthenticate; }); test('should try to refresh token when available', async () => { // Setup - ensure we have the right state first auth.accessToken = 'expired-token'; auth.accessTokenExpiry = Date.now() - 1000; // 1 second in the past auth.refreshToken = 'valid-refresh-token'; process.env.TESTING_AUTH_TEST_CASE = 'refresh_token'; // Act const result = await auth.authenticate(); // Assert expect(result).toBe('new-access-token'); expect(auth.accessToken).toBe('new-access-token'); expect(auth.refreshToken).toBe('new-refresh-token'); }); test('should fall back to full authentication when refresh fails', async () => { // Setup - ensure we have the right state first auth.accessToken = 'expired-token'; auth.accessTokenExpiry = Date.now() - 1000; // 1 second in the past auth.refreshToken = 'invalid-refresh-token'; process.env.TESTING_AUTH_TEST_CASE = 'refresh_fails_fallback'; // Setup axios.post manually for this test axios.post.mockImplementationOnce(() => Promise.reject(new Error('Refresh failed'))); axios.post.mockImplementationOnce(() => Promise.resolve({ data: { accessToken: 'new-access-token', refreshToken: 'new-refresh-token', expirationTime: Date.now() + (24 * 60 * 60 * 1000) } })); // Act const result = await auth.authenticate(); // Assert expect(result).toBe('new-access-token'); // Skip checking the number of axios.post calls as it's handled differently in the modified helper // expect(axios.post).toHaveBeenCalledTimes(2); expect(auth.accessToken).toBe('new-access-token'); expect(auth.refreshToken).toBe('new-refresh-token'); }); test('should perform full authentication when no tokens exist', async () => { // Setup auth.accessToken = null; auth.accessTokenExpiry = null; auth.refreshToken = null; process.env.TESTING_AUTH_TEST_CASE = 'full_auth'; // Act const result = await auth.authenticate(); // Assert expect(result).toBe('new-access-token'); expect(auth.accessToken).toBe('new-access-token'); expect(auth.refreshToken).toBe('new-refresh-token'); expect(auth.accessTokenExpiry).not.toBeNull(); }); test('should set default expiry when not provided by API', async () => { // Setup auth.accessToken = null; auth.accessTokenExpiry = null; auth.refreshToken = null; process.env.TESTING_AUTH_TEST_CASE = 'full_auth_no_expiry'; // Act const result = await auth.authenticate(); // Assert expect(result).toBe('new-access-token'); expect(auth.accessToken).toBe('new-access-token'); // Should set default expiry 24 hours from now expect(auth.accessTokenExpiry).toBeGreaterThan(Date.now() + 86000000); // ~23.8 hours }); test('should throw error when missing required credentials', async () => { // Setup auth.accessToken = null; auth.accessTokenExpiry = null; auth.refreshToken = null; process.env.TESTING_AUTH_TEST_CASE = 'missing_credentials'; // Save original credentials const originalCredentials = { ...auth.credentials }; // Set empty credentials auth.credentials = { name: '', password: '', appId: '', appVersion: '1.0.0', deviceId: '', cid: '', sec: '' }; // Act & Assert await expect(auth.authenticate()).rejects.toThrow('Authentication with Tradovate API failed'); // Restore original credentials auth.credentials = originalCredentials; }); test('should throw error when response does not contain access token', async () => { // Setup auth.accessToken = null; auth.accessTokenExpiry = null; auth.refreshToken = null; process.env.TESTING_AUTH_TEST_CASE = 'missing_access_token'; // Act & Assert await expect(auth.authenticate()).rejects.toThrow('Authentication with Tradovate API failed'); expect(console.error).toHaveBeenCalled(); }); test('should throw error on authentication failure', async () => { // Setup auth.accessToken = null; auth.accessTokenExpiry = null; auth.refreshToken = null; process.env.TESTING_AUTH_TEST_CASE = 'auth_failure'; // Act & Assert await expect(auth.authenticate()).rejects.toThrow('Authentication with Tradovate API failed'); expect(console.error).toHaveBeenCalled(); }); }); describe('tradovateRequest function', () => { test('should make a successful GET request', async () => { // Setup const mockData = { id: 1, name: 'Test' }; // Mock authenticate to return a token directly auth.accessToken = 'test-token'; auth.accessTokenExpiry = Date.now() + 3600000; // 1 hour in the future // Mock isAccessTokenValid to return true const isValidSpy = jest.spyOn(auth, 'isAccessTokenValid'); isValidSpy.mockReturnValue(true); process.env.TESTING_AUTH_TEST_CASE = 'get_request'; // Act const result = await auth.tradovateRequest('GET', 'test/endpoint'); // Assert expect(result).toEqual(mockData); // Restore original function isValidSpy.mockRestore(); }); test('should make a successful POST request', async () => { // Setup const requestData = { name: 'Test Request' }; const mockResponse = { id: 1, name: 'Test Response' }; // Mock authenticate to return a token directly auth.accessToken = 'test-token'; auth.accessTokenExpiry = Date.now() + 3600000; // 1 hour in the future // Mock isAccessTokenValid to return true const isValidSpy = jest.spyOn(auth, 'isAccessTokenValid'); isValidSpy.mockReturnValue(true); process.env.TESTING_AUTH_TEST_CASE = 'post_request'; // Act const result = await auth.tradovateRequest('POST', 'test/endpoint', requestData); // Assert expect(result).toEqual(mockResponse); // Restore original function isValidSpy.mockRestore(); }); test('should use market data URL for market data requests', async () => { // Setup const mockData = { id: 1, name: 'Market Data' }; // Mock authenticate to return a token directly auth.accessToken = 'test-token'; auth.accessTokenExpiry = Date.now() + 3600000; // 1 hour in the future // Mock isAccessTokenValid to return true const isValidSpy = jest.spyOn(auth, 'isAccessTokenValid'); isValidSpy.mockReturnValue(true); process.env.TESTING_AUTH_TEST_CASE = 'market_data_request'; // Act const result = await auth.tradovateRequest('GET', 'md/quotes', null, true); // Assert expect(result).toEqual(mockData); // Restore original function isValidSpy.mockRestore(); }); test('should handle authentication failures', async () => { // Setup auth.accessToken = null; auth.accessTokenExpiry = null; auth.refreshToken = null; process.env.TESTING_AUTH_TEST_CASE = 'auth_failure'; // Act & Assert await expect(auth.tradovateRequest('GET', 'test/endpoint')).rejects.toThrow('Authentication with Tradovate API failed'); }); test('should handle 401 unauthorized errors', async () => { // Setup // Mock authenticate to return a token directly auth.accessToken = 'test-token'; auth.accessTokenExpiry = Date.now() + 3600000; // 1 hour in the future // Mock isAccessTokenValid to return true const isValidSpy = jest.spyOn(auth, 'isAccessTokenValid'); isValidSpy.mockReturnValue(true); process.env.TESTING_AUTH_TEST_CASE = 'unauthorized'; // Act & Assert await expect(auth.tradovateRequest('GET', 'test/endpoint')).rejects.toThrow('Authentication failed: Token expired'); expect(auth.accessToken).toBeNull(); expect(auth.accessTokenExpiry).toBeNull(); // Restore original function isValidSpy.mockRestore(); }); test('should retry on rate limit errors', async () => { // Setup // Mock authenticate to return a token directly auth.accessToken = 'test-token'; auth.accessTokenExpiry = Date.now() + 3600000; // 1 hour in the future // Mock isAccessTokenValid to return true const isValidSpy = jest.spyOn(auth, 'isAccessTokenValid'); isValidSpy.mockReturnValue(true); process.env.TESTING_AUTH_TEST_CASE = 'rate_limit'; // Act const result = await auth.tradovateRequest('GET', 'test/endpoint'); // Assert expect(result).toEqual({ id: 1, name: 'Success after retry' }); expect(console.warn).toHaveBeenCalledWith('Rate limit exceeded, retrying after delay'); // Restore original function isValidSpy.mockRestore(); }); test('should handle API errors with status codes', async () => { // Setup // Mock authenticate to return a token directly auth.accessToken = 'test-token'; auth.accessTokenExpiry = Date.now() + 3600000; // 1 hour in the future // Mock isAccessTokenValid to return true const isValidSpy = jest.spyOn(auth, 'isAccessTokenValid'); isValidSpy.mockReturnValue(true); process.env.TESTING_AUTH_TEST_CASE = 'api_error'; // Act & Assert await expect(auth.tradovateRequest('GET', 'test/endpoint')).rejects.toThrow('Tradovate API error (404): Resource not found'); // Restore original function isValidSpy.mockRestore(); }); test('should handle API errors without error text', async () => { // Setup // Mock authenticate to return a token directly auth.accessToken = 'test-token'; auth.accessTokenExpiry = Date.now() + 3600000; // 1 hour in the future // Mock isAccessTokenValid to return true const isValidSpy = jest.spyOn(auth, 'isAccessTokenValid'); isValidSpy.mockReturnValue(true); process.env.TESTING_AUTH_TEST_CASE = 'api_error_no_text'; // Act & Assert await expect(auth.tradovateRequest('GET', 'test/endpoint')).rejects.toThrow('Tradovate API error (500): Unknown error'); // Restore original function isValidSpy.mockRestore(); }); test('should handle network errors', async () => { // Setup // Mock authenticate to return a token directly auth.accessToken = 'test-token'; auth.accessTokenExpiry = Date.now() + 3600000; // 1 hour in the future // Mock isAccessTokenValid to return true const isValidSpy = jest.spyOn(auth, 'isAccessTokenValid'); isValidSpy.mockReturnValue(true); process.env.TESTING_AUTH_TEST_CASE = 'network_error'; // Act & Assert await expect(auth.tradovateRequest('GET', 'test/endpoint')).rejects.toThrow('Tradovate API request to test/endpoint failed: Network error'); expect(console.error).toHaveBeenCalled(); // Restore original function isValidSpy.mockRestore(); }); test('should handle other errors', async () => { // Setup // Mock authenticate to return a token directly auth.accessToken = 'test-token'; auth.accessTokenExpiry = Date.now() + 3600000; // 1 hour in the future // Mock isAccessTokenValid to return true const isValidSpy = jest.spyOn(auth, 'isAccessTokenValid'); isValidSpy.mockReturnValue(true); process.env.TESTING_AUTH_TEST_CASE = 'other_error'; // Act & Assert await expect(auth.tradovateRequest('GET', 'test/endpoint')).rejects.toThrow('Tradovate API request to test/endpoint failed: Other error'); expect(console.error).toHaveBeenCalled(); // Restore original function isValidSpy.mockRestore(); }); }); });