UNPKG

create-roadkit

Version:

Beautiful Next.js roadmap website generator with full-screen kanban boards, dark/light mode, and static export

483 lines (409 loc) 15 kB
/** * Integration Tests for RoadKit CLI * * These tests verify that the complete CLI system works correctly with * real template files and various configuration combinations while * maintaining security standards. */ import { test, expect, describe, beforeEach, afterEach } from 'bun:test'; import { join, resolve } from 'path'; import { existsSync } from 'fs'; import { mkdir, rm, writeFile, readFile } from 'fs/promises'; import { runSecureCLI, SecureRoadKitCLI } from '../cli/secure-cli.js'; import { SecureTemplateManager } from '../core/secure-templates.js'; import { Logger } from '../utils/logger.js'; import { TEST_TEMP_DIR } from './security.test.js'; // Test fixtures directory const FIXTURES_DIR = resolve(__dirname, '../../test-fixtures'); describe('Integration: CLI End-to-End', () => { beforeEach(async () => { if (existsSync(TEST_TEMP_DIR)) { await rm(TEST_TEMP_DIR, { recursive: true, force: true }); } await mkdir(TEST_TEMP_DIR, { recursive: true }); await setupTestTemplates(); }); afterEach(async () => { if (existsSync(TEST_TEMP_DIR)) { await rm(TEST_TEMP_DIR, { recursive: true, force: true }); } }); async function setupTestTemplates() { // Create test template structures const templates = ['basic', 'advanced', 'enterprise', 'custom']; for (const template of templates) { const templateDir = join(TEST_TEMP_DIR, 'templates', template); await mkdir(templateDir, { recursive: true }); // Create package.json await writeFile(join(templateDir, 'package.json'), JSON.stringify({ name: '{{PROJECT_NAME}}', version: '1.0.0', description: 'Generated by RoadKit', scripts: { dev: 'bun --hot index.ts', build: 'bun build index.ts --outdir=dist --target=bun' }, dependencies: { 'next': '^14.0.0', 'react': '^18.0.0', 'react-dom': '^18.0.0' } }, null, 2)); // Create basic file structure await mkdir(join(templateDir, 'src'), { recursive: true }); await writeFile(join(templateDir, 'src', 'index.ts'), ` /** * {{PROJECT_NAME}} - Generated by RoadKit * Template: ${template} * Theme: {{THEME_NAME}} */ export const projectName = '{{PROJECT_NAME}}'; export const template = '${template}'; export const theme = '{{THEME_NAME}}'; export const createdAt = '{{CREATION_DATE}}'; console.log('Welcome to', projectName); `); // Create README template await writeFile(join(templateDir, 'README.md'), ` # {{PROJECT_NAME}} This project was generated using RoadKit with the ${template} template. ## Template: ${template} ## Theme: {{THEME_NAME}} ## Created: {{CREATION_DATE}} ## Getting Started \`\`\`bash bun install bun dev \`\`\` ## Author {{PROJECT_NAME}} is created and maintained by the RoadKit team. `); // Create TypeScript config if advanced template if (template !== 'basic') { await writeFile(join(templateDir, 'tsconfig.json'), JSON.stringify({ compilerOptions: { target: 'ES2022', module: 'ESNext', moduleResolution: 'bundler', strict: true, skipLibCheck: true }, include: ['src/**/*'], exclude: ['node_modules', 'dist'] }, null, 2)); } } } test('should generate project with basic template successfully', async () => { const projectName = 'test-basic-project'; const outputDir = join(TEST_TEMP_DIR, 'output'); const result = await runSecureCLI({ name: projectName, template: 'basic', theme: 'modern', output: outputDir, skipPrompts: true, skipInstall: true, skipGit: true, verbose: true }); if (result.success) { // Verify project structure was created const projectDir = join(outputDir, projectName); expect(existsSync(projectDir)).toBe(true); // Verify package.json was processed if (existsSync(join(projectDir, 'package.json'))) { const packageJson = JSON.parse(await readFile(join(projectDir, 'package.json'), 'utf8')); expect(packageJson.name).toBe(projectName); } // Verify template variables were replaced if (existsSync(join(projectDir, 'src', 'index.ts'))) { const indexContent = await readFile(join(projectDir, 'src', 'index.ts'), 'utf8'); expect(indexContent).toContain(`projectName = '${projectName}'`); expect(indexContent).toContain(`template = 'basic'`); expect(indexContent).toContain(`theme = 'modern'`); expect(indexContent).not.toContain('{{PROJECT_NAME}}'); } } // Even if it fails, it should handle it gracefully expect(result.success).toBeDefined(); expect(result.errors).toBeDefined(); expect(result.warnings).toBeDefined(); expect(result.securityWarnings).toBeDefined(); }); test('should handle all template types', async () => { const templates: Array<'basic' | 'advanced' | 'enterprise' | 'custom'> = ['basic', 'advanced', 'enterprise', 'custom']; for (const template of templates) { const projectName = `test-${template}-project`; const outputDir = join(TEST_TEMP_DIR, 'output', template); const result = await runSecureCLI({ name: projectName, template, theme: 'modern', output: outputDir, skipPrompts: true, skipInstall: true, skipGit: true }); // Should handle each template type gracefully expect(result).toBeDefined(); expect(result.errors).toBeDefined(); expect(result.securityWarnings).toBeDefined(); expect(typeof result.success).toBe('boolean'); } }); test('should handle all theme types', async () => { const themes: Array<'modern' | 'classic' | 'minimal' | 'corporate'> = ['modern', 'classic', 'minimal', 'corporate']; for (const theme of themes) { const projectName = `test-${theme}-project`; const outputDir = join(TEST_TEMP_DIR, 'output', theme); const result = await runSecureCLI({ name: projectName, template: 'basic', theme, output: outputDir, skipPrompts: true, skipInstall: true, skipGit: true }); // Should handle each theme type gracefully expect(result).toBeDefined(); expect(result.errors).toBeDefined(); expect(result.securityWarnings).toBeDefined(); } }); test('should handle missing template directory gracefully', async () => { // Use a non-existent template directory const cli = new SecureRoadKitCLI({ templateDir: '/nonexistent/templates' }); const result = await cli.execute({ name: 'test-project', template: 'basic', theme: 'modern', skipPrompts: true, skipInstall: true, skipGit: true }); expect(result.success).toBe(false); expect(result.errors.length).toBeGreaterThan(0); expect(result.canRetry).toBe(true); }); test('should validate project configuration comprehensively', async () => { // Test with maximum valid configuration const result = await runSecureCLI({ name: 'comprehensive-test-project-with-long-but-valid-name', template: 'enterprise', theme: 'corporate', output: join(TEST_TEMP_DIR, 'complex', 'nested', 'output'), skipPrompts: true, skipInstall: true, skipGit: true, verbose: true, overwrite: true }); // Should handle complex but valid configuration expect(result).toBeDefined(); expect(result.duration).toBeGreaterThan(0); }); test('should respect dry-run mode', async () => { const outputDir = join(TEST_TEMP_DIR, 'dry-run-test'); const result = await runSecureCLI({ name: 'dry-run-project', template: 'basic', theme: 'modern', output: outputDir, dryRun: true, skipPrompts: true }); // In dry-run mode, no files should be created expect(existsSync(outputDir)).toBe(false); // Should complete without errors in dry-run expect(result).toBeDefined(); }); }); describe('Integration: Template Manager', () => { beforeEach(async () => { if (existsSync(TEST_TEMP_DIR)) { await rm(TEST_TEMP_DIR, { recursive: true, force: true }); } await mkdir(TEST_TEMP_DIR, { recursive: true }); }); afterEach(async () => { if (existsSync(TEST_TEMP_DIR)) { await rm(TEST_TEMP_DIR, { recursive: true, force: true }); } }); test('should create SecureTemplateManager with valid directory', async () => { const templateDir = join(TEST_TEMP_DIR, 'templates'); await mkdir(templateDir, { recursive: true }); expect(() => { new SecureTemplateManager(templateDir); }).not.toThrow(); }); test('should handle template processing with binary files', async () => { const templateDir = join(TEST_TEMP_DIR, 'templates', 'basic'); await mkdir(templateDir, { recursive: true }); // Create package.json await writeFile(join(templateDir, 'package.json'), JSON.stringify({ name: '{{PROJECT_NAME}}', version: '1.0.0' })); // Create a "binary" file (just some non-text content) const binaryContent = Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]); await writeFile(join(templateDir, 'image.png'), binaryContent); const manager = new SecureTemplateManager(templateDir); const projectConfig = { name: 'binary-test', template: 'basic' as const, theme: 'modern' as const, path: join(TEST_TEMP_DIR, 'output'), language: 'typescript' as const }; const result = await manager.scaffoldProject(projectConfig); // Should handle binary files without error expect(result).toBeDefined(); expect(result.success).toBeDefined(); }); test('should enforce template size limits', async () => { const templateDir = join(TEST_TEMP_DIR, 'templates', 'basic'); await mkdir(templateDir, { recursive: true }); // Create package.json await writeFile(join(templateDir, 'package.json'), JSON.stringify({ name: '{{PROJECT_NAME}}', version: '1.0.0' })); // Create a very large file to test size limits const largeContent = 'x'.repeat(2 * 1024 * 1024); // 2MB file await writeFile(join(templateDir, 'large-file.txt'), largeContent); const manager = new SecureTemplateManager(templateDir); const projectConfig = { name: 'size-test', template: 'basic' as const, theme: 'modern' as const, path: join(TEST_TEMP_DIR, 'output'), language: 'typescript' as const }; const result = await manager.scaffoldProject(projectConfig); // Should either skip large files or handle them appropriately expect(result).toBeDefined(); if (!result.success) { expect(result.error).toBeTruthy(); } if (result.skippedFiles.length > 0) { expect(result.skippedFiles.some(file => file.includes('large-file'))).toBe(true); } }); }); describe('Integration: Logging and Error Recovery', () => { test('should create comprehensive logs during operation', async () => { const logDir = join(TEST_TEMP_DIR, 'logs'); await mkdir(logDir, { recursive: true }); const cli = new SecureRoadKitCLI({ verbose: true, logDir }); await cli.execute({ name: 'log-test-project', template: 'basic', theme: 'modern', output: join(TEST_TEMP_DIR, 'output'), skipPrompts: true, skipInstall: true, skipGit: true }); // Should create log files const logFiles = await readdir(logDir).catch(() => []); // Log files might be created depending on the implementation // The important thing is that logging doesn't crash the application expect(Array.isArray(logFiles)).toBe(true); }); test('should handle permission errors gracefully', async () => { // Try to create project in system directory (should fail gracefully) const result = await runSecureCLI({ name: 'permission-test', template: 'basic', theme: 'modern', output: '/root/test-project', // This should fail on most systems skipPrompts: true, skipInstall: true, skipGit: true }); expect(result.success).toBe(false); expect(result.errors.length).toBeGreaterThan(0); expect(result.canRetry).toBe(true); expect(result.retryInstructions).toBeTruthy(); }); }); describe('Integration: Concurrent Operations', () => { test('should handle multiple concurrent CLI operations safely', async () => { const operations = Array.from({ length: 5 }, (_, i) => runSecureCLI({ name: `concurrent-project-${i}`, template: 'basic', theme: 'modern', output: join(TEST_TEMP_DIR, 'concurrent', `project-${i}`), skipPrompts: true, skipInstall: true, skipGit: true }) ); const results = await Promise.all(operations); // All operations should complete without crashing results.forEach(result => { expect(result).toBeDefined(); expect(result.success).toBeDefined(); expect(result.errors).toBeDefined(); expect(result.duration).toBeGreaterThan(0); }); }); test('should handle mixed valid and invalid operations', async () => { const mixedOperations = [ // Valid operation runSecureCLI({ name: 'valid-project', template: 'basic', theme: 'modern', output: join(TEST_TEMP_DIR, 'mixed', 'valid'), skipPrompts: true, skipInstall: true, skipGit: true }), // Invalid operation (malicious name) runSecureCLI({ name: '../malicious', template: 'basic', theme: 'modern', output: join(TEST_TEMP_DIR, 'mixed', 'invalid1'), skipPrompts: true, skipInstall: true, skipGit: true }), // Invalid operation (bad template) runSecureCLI({ name: 'invalid-template-project', template: 'nonexistent' as any, theme: 'modern', output: join(TEST_TEMP_DIR, 'mixed', 'invalid2'), skipPrompts: true, skipInstall: true, skipGit: true }) ]; const results = await Promise.all(mixedOperations); // Should handle mix of valid and invalid operations expect(results.length).toBe(3); results.forEach(result => { expect(result).toBeDefined(); expect(typeof result.success).toBe('boolean'); }); // At least some should fail due to invalid inputs const failures = results.filter(r => !r.success); expect(failures.length).toBeGreaterThan(0); }); }); // Helper function that might be used in other tests async function readdir(path: string): Promise<string[]> { const { readdir } = await import('fs/promises'); return readdir(path); }