Tradovate MCP Server
by alexanimal
Verified
- tradovate-mcp-server
- tests
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();
});
});
});