- 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.
358 lines
9.8 KiB
TypeScript
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();
|
|
} |