Files
n8n-mcp/tests/setup/test-env.ts
czlonkowski a8c3d04c12 fix: resolve test environment loading race condition in CI
- Move getTestConfig() calls from module level to test execution time
- Add CI-specific debug logging to diagnose environment loading issues
- Add verification step in CI workflow to check .env.test availability
- Ensure environment variables are loaded before tests access config

The issue was that config was being accessed at module import time,
which could happen before the global setup runs in some CI environments.
2025-07-29 07:13:37 +02:00

358 lines
9.8 KiB
TypeScript

/**
* Test Environment Configuration Loader
*
* This module handles loading and validating test environment variables
* with type safety and default values.
*/
import * as dotenv from 'dotenv';
import * as path from 'path';
import { existsSync } from 'fs';
// Load test environment variables
export function loadTestEnvironment(): void {
// CI Debug logging
const isCI = process.env.CI === 'true';
// Load base test environment
const testEnvPath = path.resolve(process.cwd(), '.env.test');
if (isCI) {
console.log('[CI-DEBUG] Looking for .env.test at:', testEnvPath);
console.log('[CI-DEBUG] File exists?', existsSync(testEnvPath));
}
if (existsSync(testEnvPath)) {
const result = dotenv.config({ path: testEnvPath });
if (isCI && result.error) {
console.error('[CI-DEBUG] Failed to load .env.test:', result.error);
} else if (isCI && result.parsed) {
console.log('[CI-DEBUG] Successfully loaded', Object.keys(result.parsed).length, 'env vars from .env.test');
}
} else if (isCI) {
console.warn('[CI-DEBUG] .env.test file not found, will use defaults only');
}
// Load local test overrides (for sensitive values)
const localEnvPath = path.resolve(process.cwd(), '.env.test.local');
if (existsSync(localEnvPath)) {
dotenv.config({ path: localEnvPath, override: true });
}
// Set test-specific defaults
setTestDefaults();
// Validate required environment variables
validateTestEnvironment();
}
/**
* Set default values for test environment variables
*/
function setTestDefaults(): void {
// Ensure we're in test mode
process.env.NODE_ENV = 'test';
process.env.TEST_ENVIRONMENT = 'true';
// Set defaults if not already set
const defaults: Record<string, string> = {
// Database
NODE_DB_PATH: ':memory:',
REBUILD_ON_START: 'false',
// API
N8N_API_URL: 'http://localhost:3001/mock-api',
N8N_API_KEY: 'test-api-key-12345',
// Server
PORT: '3001',
HOST: '127.0.0.1',
// Logging
LOG_LEVEL: 'error',
DEBUG: 'false',
TEST_LOG_VERBOSE: 'false',
// Timeouts
TEST_TIMEOUT_UNIT: '5000',
TEST_TIMEOUT_INTEGRATION: '15000',
TEST_TIMEOUT_E2E: '30000',
TEST_TIMEOUT_GLOBAL: '60000',
// Test execution
TEST_RETRY_ATTEMPTS: '2',
TEST_RETRY_DELAY: '1000',
TEST_PARALLEL: 'true',
TEST_MAX_WORKERS: '4',
// Features
FEATURE_MOCK_EXTERNAL_APIS: 'true',
FEATURE_USE_TEST_CONTAINERS: 'false',
MSW_ENABLED: 'true',
MSW_API_DELAY: '0',
// Paths
TEST_FIXTURES_PATH: './tests/fixtures',
TEST_DATA_PATH: './tests/data',
TEST_SNAPSHOTS_PATH: './tests/__snapshots__',
// Performance
PERF_THRESHOLD_API_RESPONSE: '100',
PERF_THRESHOLD_DB_QUERY: '50',
PERF_THRESHOLD_NODE_PARSE: '200',
// Caching
CACHE_TTL: '0',
CACHE_ENABLED: 'false',
// Rate limiting
RATE_LIMIT_MAX: '0',
RATE_LIMIT_WINDOW: '0',
// Error handling
ERROR_SHOW_STACK: 'true',
ERROR_SHOW_DETAILS: 'true',
// Cleanup
TEST_CLEANUP_ENABLED: 'true',
TEST_CLEANUP_ON_FAILURE: 'false',
// Database seeding
TEST_SEED_DATABASE: 'true',
TEST_SEED_TEMPLATES: 'true',
// Network
NETWORK_TIMEOUT: '5000',
NETWORK_RETRY_COUNT: '0',
// Memory
TEST_MEMORY_LIMIT: '512',
// Coverage
COVERAGE_DIR: './coverage',
COVERAGE_REPORTER: 'lcov,html,text-summary'
};
for (const [key, value] of Object.entries(defaults)) {
if (!process.env[key]) {
process.env[key] = value;
}
}
}
/**
* Validate that required environment variables are set
*/
function validateTestEnvironment(): void {
const required = [
'NODE_ENV',
'NODE_DB_PATH',
'N8N_API_URL',
'N8N_API_KEY'
];
const missing = required.filter(key => !process.env[key]);
if (missing.length > 0) {
throw new Error(
`Missing required test environment variables: ${missing.join(', ')}\n` +
'Please ensure .env.test is properly configured.'
);
}
// Validate NODE_ENV is set to test
if (process.env.NODE_ENV !== 'test') {
throw new Error(
'NODE_ENV must be set to "test" when running tests.\n' +
'This prevents accidental execution against production systems.'
);
}
}
/**
* Get typed test environment configuration
*/
export function getTestConfig() {
return {
// Environment
nodeEnv: process.env.NODE_ENV!,
isTest: process.env.TEST_ENVIRONMENT === 'true',
// Database
database: {
path: process.env.NODE_DB_PATH!,
rebuildOnStart: process.env.REBUILD_ON_START === 'true',
seedData: process.env.TEST_SEED_DATABASE === 'true',
seedTemplates: process.env.TEST_SEED_TEMPLATES === 'true'
},
// API
api: {
url: process.env.N8N_API_URL!,
key: process.env.N8N_API_KEY!,
webhookBaseUrl: process.env.N8N_WEBHOOK_BASE_URL,
webhookTestUrl: process.env.N8N_WEBHOOK_TEST_URL
},
// Server
server: {
port: parseInt(process.env.PORT || '3001', 10),
host: process.env.HOST || '127.0.0.1',
corsOrigin: process.env.CORS_ORIGIN?.split(',') || []
},
// Authentication
auth: {
token: process.env.AUTH_TOKEN,
mcpToken: process.env.MCP_AUTH_TOKEN
},
// Logging
logging: {
level: process.env.LOG_LEVEL || 'error',
debug: process.env.DEBUG === 'true',
verbose: process.env.TEST_LOG_VERBOSE === 'true',
showStack: process.env.ERROR_SHOW_STACK === 'true',
showDetails: process.env.ERROR_SHOW_DETAILS === 'true'
},
// Test execution
execution: {
timeouts: {
unit: parseInt(process.env.TEST_TIMEOUT_UNIT || '5000', 10),
integration: parseInt(process.env.TEST_TIMEOUT_INTEGRATION || '15000', 10),
e2e: parseInt(process.env.TEST_TIMEOUT_E2E || '30000', 10),
global: parseInt(process.env.TEST_TIMEOUT_GLOBAL || '60000', 10)
},
retry: {
attempts: parseInt(process.env.TEST_RETRY_ATTEMPTS || '2', 10),
delay: parseInt(process.env.TEST_RETRY_DELAY || '1000', 10)
},
parallel: process.env.TEST_PARALLEL === 'true',
maxWorkers: parseInt(process.env.TEST_MAX_WORKERS || '4', 10)
},
// Features
features: {
coverage: process.env.FEATURE_TEST_COVERAGE === 'true',
screenshots: process.env.FEATURE_TEST_SCREENSHOTS === 'true',
videos: process.env.FEATURE_TEST_VIDEOS === 'true',
trace: process.env.FEATURE_TEST_TRACE === 'true',
mockExternalApis: process.env.FEATURE_MOCK_EXTERNAL_APIS === 'true',
useTestContainers: process.env.FEATURE_USE_TEST_CONTAINERS === 'true'
},
// Mocking
mocking: {
msw: {
enabled: process.env.MSW_ENABLED === 'true',
apiDelay: parseInt(process.env.MSW_API_DELAY || '0', 10)
},
redis: {
enabled: process.env.REDIS_MOCK_ENABLED === 'true',
port: parseInt(process.env.REDIS_MOCK_PORT || '6380', 10)
},
elasticsearch: {
enabled: process.env.ELASTICSEARCH_MOCK_ENABLED === 'true',
port: parseInt(process.env.ELASTICSEARCH_MOCK_PORT || '9201', 10)
}
},
// Paths
paths: {
fixtures: process.env.TEST_FIXTURES_PATH || './tests/fixtures',
data: process.env.TEST_DATA_PATH || './tests/data',
snapshots: process.env.TEST_SNAPSHOTS_PATH || './tests/__snapshots__'
},
// Performance
performance: {
thresholds: {
apiResponse: parseInt(process.env.PERF_THRESHOLD_API_RESPONSE || '100', 10),
dbQuery: parseInt(process.env.PERF_THRESHOLD_DB_QUERY || '50', 10),
nodeParse: parseInt(process.env.PERF_THRESHOLD_NODE_PARSE || '200', 10)
}
},
// Rate limiting
rateLimiting: {
max: parseInt(process.env.RATE_LIMIT_MAX || '0', 10),
window: parseInt(process.env.RATE_LIMIT_WINDOW || '0', 10)
},
// Caching
cache: {
enabled: process.env.CACHE_ENABLED === 'true',
ttl: parseInt(process.env.CACHE_TTL || '0', 10)
},
// Cleanup
cleanup: {
enabled: process.env.TEST_CLEANUP_ENABLED === 'true',
onFailure: process.env.TEST_CLEANUP_ON_FAILURE === 'true'
},
// Network
network: {
timeout: parseInt(process.env.NETWORK_TIMEOUT || '5000', 10),
retryCount: parseInt(process.env.NETWORK_RETRY_COUNT || '0', 10)
},
// Memory
memory: {
limit: parseInt(process.env.TEST_MEMORY_LIMIT || '512', 10)
},
// Coverage
coverage: {
dir: process.env.COVERAGE_DIR || './coverage',
reporters: (process.env.COVERAGE_REPORTER || 'lcov,html,text-summary').split(',')
}
};
}
// Export type for the test configuration
export type TestConfig = ReturnType<typeof getTestConfig>;
/**
* Helper to check if we're in test mode
*/
export function isTestMode(): boolean {
return process.env.NODE_ENV === 'test' || process.env.TEST_ENVIRONMENT === 'true';
}
/**
* Helper to get timeout for specific test type
*/
export function getTestTimeout(type: 'unit' | 'integration' | 'e2e' | 'global' = 'unit'): number {
const config = getTestConfig();
return config.execution.timeouts[type];
}
/**
* Helper to check if a feature is enabled
*/
export function isFeatureEnabled(feature: keyof TestConfig['features']): boolean {
const config = getTestConfig();
return config.features[feature];
}
/**
* Reset environment to defaults (useful for test isolation)
*/
export function resetTestEnvironment(): void {
// Clear all test-specific environment variables
const testKeys = Object.keys(process.env).filter(key =>
key.startsWith('TEST_') ||
key.startsWith('FEATURE_') ||
key.startsWith('MSW_') ||
key.startsWith('PERF_')
);
testKeys.forEach(key => {
delete process.env[key];
});
// Reload defaults
loadTestEnvironment();
}