/** * Integration Tests: Config Wizard End-to-End Flows * * Feature: config-wizard * * Tests filesystem interactions, real-world data parsing, and end-to-end * function composition from `configure.js`. * * Validates: Requirements 1.4, 1.5, 6.5, 6.6, 9.6, 9.7, 14.4, 16.2, 16.4, 16.6 */ const os = require('os'); const fs = require('fs'); const path = require('path'); const { VARIABLE_DESCRIPTORS, GROUP_ORDER, OPTIONAL_GROUPS, parseEnvFile, parseDockerCompose, generateEnvContent, writeEnvFile, createBackup, detectInfraState, shouldSkipFrontendBuild, checkNodeVersion, checkProjectRoot, } = require('../../configure.js'); /** * Create a temporary directory for test isolation. * Returns the path to the created directory. */ function createTempDir() { return fs.mkdtempSync(path.join(os.tmpdir(), 'config-wizard-test-')); } /** * Recursively remove a directory and its contents. */ function removeTempDir(dirPath) { fs.rmSync(dirPath, { recursive: true, force: true }); } // ───────────────────────────────────────────────────────────────────────────── // Test 1: Full wizard run with all defaults — verify correct files written // ───────────────────────────────────────────────────────────────────────────── describe('Full wizard run with all defaults', () => { let tmpDir; beforeEach(() => { tmpDir = createTempDir(); }); afterEach(() => { removeTempDir(tmpDir); }); test('generateEnvContent + writeEnvFile produces valid backend .env with all required defaults', () => { const values = new Map(); values.set('PORT', '3001'); values.set('API_HOST', 'localhost'); values.set('CORS_ORIGINS', 'http://localhost:3000'); values.set('DATABASE_URL', 'postgresql://steam:pass@localhost:5433/cve_dashboard'); values.set('SESSION_SECRET', 'a-very-long-secret-key-here-1234'); values.set('REACT_APP_API_BASE', 'http://localhost:3001/api'); values.set('REACT_APP_API_HOST', 'http://localhost:3001'); const backendDescriptors = VARIABLE_DESCRIPTORS.filter(d => d.target === 'backend'); const content = generateEnvContent(values, GROUP_ORDER, backendDescriptors, []); const filePath = path.join(tmpDir, '.env'); writeEnvFile(filePath, content); expect(fs.existsSync(filePath)).toBe(true); const written = fs.readFileSync(filePath, 'utf8'); // Verify key values are present expect(written).toContain('PORT=3001'); expect(written).toContain('API_HOST=localhost'); expect(written).toContain('CORS_ORIGINS=http://localhost:3000'); expect(written).toContain('SESSION_SECRET=a-very-long-secret-key-here-1234'); // DATABASE_URL contains special chars, should be quoted expect(written).toContain('DATABASE_URL='); // Ends with newline expect(written.endsWith('\n')).toBe(true); }); test('generateEnvContent + writeEnvFile produces valid frontend .env with defaults', () => { const values = new Map(); values.set('REACT_APP_API_BASE', 'http://localhost:3001/api'); values.set('REACT_APP_API_HOST', 'http://localhost:3001'); const frontendDescriptors = VARIABLE_DESCRIPTORS.filter(d => d.target === 'frontend'); const content = generateEnvContent(values, GROUP_ORDER, frontendDescriptors, []); const filePath = path.join(tmpDir, 'frontend.env'); writeEnvFile(filePath, content); const written = fs.readFileSync(filePath, 'utf8'); expect(written).toContain('REACT_APP_API_BASE=http://localhost:3001/api'); expect(written).toContain('REACT_APP_API_HOST=http://localhost:3001'); expect(written).toContain('# --- Frontend Settings ---'); }); }); // ───────────────────────────────────────────────────────────────────────────── // Test 2: Wizard with existing .env files — values pre-filled correctly // ───────────────────────────────────────────────────────────────────────────── describe('Wizard with existing .env files', () => { let tmpDir; beforeEach(() => { tmpDir = createTempDir(); }); afterEach(() => { removeTempDir(tmpDir); }); test('parseEnvFile reads existing values correctly', () => { const envContent = [ 'PORT=4000', 'API_HOST=192.168.1.100', 'CORS_ORIGINS=http://myhost:3000', 'DATABASE_URL="postgresql://user:pass@localhost:5433/mydb"', 'SESSION_SECRET=my-super-secret-session-key-123', 'MY_CUSTOM_VAR=preserved', ].join('\n'); const filePath = path.join(tmpDir, '.env'); fs.writeFileSync(filePath, envContent, 'utf8'); const result = parseEnvFile(filePath); expect(result.managed.get('PORT')).toBe('4000'); expect(result.managed.get('API_HOST')).toBe('192.168.1.100'); expect(result.managed.get('CORS_ORIGINS')).toBe('http://myhost:3000'); expect(result.managed.get('DATABASE_URL')).toBe('postgresql://user:pass@localhost:5433/mydb'); expect(result.managed.get('SESSION_SECRET')).toBe('my-super-secret-session-key-123'); expect(result.unmanaged).toContain('MY_CUSTOM_VAR=preserved'); }); test('parseEnvFile handles quoted values with spaces', () => { const envContent = 'CORS_ORIGINS="http://localhost:3000, http://localhost:8080"\n'; const filePath = path.join(tmpDir, '.env'); fs.writeFileSync(filePath, envContent, 'utf8'); const result = parseEnvFile(filePath); expect(result.managed.get('CORS_ORIGINS')).toBe('http://localhost:3000, http://localhost:8080'); }); test('parseEnvFile returns empty maps for non-existent file', () => { const result = parseEnvFile(path.join(tmpDir, 'nonexistent.env')); expect(result.managed.size).toBe(0); expect(result.unmanaged.length).toBe(0); }); }); // ───────────────────────────────────────────────────────────────────────────── // Test 3: Wizard with skipped groups — groups absent from output // ───────────────────────────────────────────────────────────────────────────── describe('Wizard with skipped groups', () => { test('generateEnvContent excludes variables from skipped groups', () => { const values = new Map(); values.set('PORT', '3001'); values.set('API_HOST', 'localhost'); values.set('CORS_ORIGINS', 'http://localhost:3000'); values.set('DATABASE_URL', 'postgresql://steam:pass@localhost:5433/cve_dashboard'); values.set('SESSION_SECRET', 'a-very-long-secret-key-here-1234'); // Intentionally NOT setting any Ivanti, Atlas, Jira, CARD, GitLab, NVD values const backendDescriptors = VARIABLE_DESCRIPTORS.filter(d => d.target === 'backend'); const content = generateEnvContent(values, GROUP_ORDER, backendDescriptors, []); // Skipped groups should not appear in output expect(content).not.toContain('# --- NVD API ---'); expect(content).not.toContain('# --- Ivanti Integration ---'); expect(content).not.toContain('# --- Atlas Integration ---'); expect(content).not.toContain('# --- Jira Integration ---'); expect(content).not.toContain('# --- CARD Integration ---'); expect(content).not.toContain('# --- GitLab Integration ---'); // Required groups should still be present expect(content).toContain('# --- Core Settings ---'); expect(content).toContain('# --- Database ---'); expect(content).toContain('# --- Session ---'); }); test('generateEnvContent includes optional group when values are provided', () => { const values = new Map(); values.set('PORT', '3001'); values.set('API_HOST', 'localhost'); values.set('CORS_ORIGINS', 'http://localhost:3000'); values.set('DATABASE_URL', 'postgresql://steam:pass@localhost:5433/cve_dashboard'); values.set('SESSION_SECRET', 'a-very-long-secret-key-here-1234'); values.set('NVD_API_KEY', 'my-nvd-key-12345'); const backendDescriptors = VARIABLE_DESCRIPTORS.filter(d => d.target === 'backend'); const content = generateEnvContent(values, GROUP_ORDER, backendDescriptors, []); expect(content).toContain('# --- NVD API ---'); expect(content).toContain('NVD_API_KEY=my-nvd-key-12345'); }); }); // ───────────────────────────────────────────────────────────────────────────── // Test 4: Missing project structure — error exit // ───────────────────────────────────────────────────────────────────────────── describe('Missing project structure', () => { let tmpDir; let originalCwd; let mockExit; beforeEach(() => { tmpDir = createTempDir(); originalCwd = process.cwd(); mockExit = jest.spyOn(process, 'exit').mockImplementation(() => { throw new Error('process.exit called'); }); }); afterEach(() => { process.chdir(originalCwd); mockExit.mockRestore(); removeTempDir(tmpDir); }); test('checkProjectRoot exits when backend/ is missing', () => { // Create only frontend/ fs.mkdirSync(path.join(tmpDir, 'frontend')); process.chdir(tmpDir); expect(() => checkProjectRoot()).toThrow('process.exit called'); expect(mockExit).toHaveBeenCalledWith(1); }); test('checkProjectRoot exits when frontend/ is missing', () => { // Create only backend/ fs.mkdirSync(path.join(tmpDir, 'backend')); process.chdir(tmpDir); expect(() => checkProjectRoot()).toThrow('process.exit called'); expect(mockExit).toHaveBeenCalledWith(1); }); test('checkProjectRoot exits when both are missing', () => { process.chdir(tmpDir); expect(() => checkProjectRoot()).toThrow('process.exit called'); expect(mockExit).toHaveBeenCalledWith(1); }); test('checkProjectRoot succeeds when both directories exist', () => { fs.mkdirSync(path.join(tmpDir, 'backend')); fs.mkdirSync(path.join(tmpDir, 'frontend')); process.chdir(tmpDir); expect(() => checkProjectRoot()).not.toThrow(); }); }); // ───────────────────────────────────────────────────────────────────────────── // Test 5: File write permission error — graceful failure // ───────────────────────────────────────────────────────────────────────────── describe('File write permission error', () => { let tmpDir; beforeEach(() => { tmpDir = createTempDir(); }); afterEach(() => { // Restore permissions before cleanup try { fs.chmodSync(path.join(tmpDir, 'readonly'), 0o755); } catch { /* ignore */ } removeTempDir(tmpDir); }); test('writeEnvFile throws on invalid path (non-existent nested directory)', () => { // Use a deeply nested non-existent path that will fail regardless of user const filePath = path.join(tmpDir, 'no', 'such', 'deep', 'path', '.env'); expect(() => writeEnvFile(filePath, 'PORT=3001\n')).toThrow(); }); test('writeEnvFile succeeds on valid writable path', () => { const filePath = path.join(tmpDir, '.env'); expect(() => writeEnvFile(filePath, 'PORT=3001\n')).not.toThrow(); expect(fs.readFileSync(filePath, 'utf8')).toBe('PORT=3001\n'); }); }); // ───────────────────────────────────────────────────────────────────────────── // Test 6: Infrastructure state detection // ───────────────────────────────────────────────────────────────────────────── describe('Infrastructure state detection', () => { let tmpDir; let originalCwd; beforeEach(() => { tmpDir = createTempDir(); originalCwd = process.cwd(); }); afterEach(() => { process.chdir(originalCwd); removeTempDir(tmpDir); }); test('detectInfraState returns correct values based on filesystem state', () => { // Set up a minimal project structure fs.mkdirSync(path.join(tmpDir, 'backend')); fs.mkdirSync(path.join(tmpDir, 'frontend')); fs.mkdirSync(path.join(tmpDir, 'backend', 'node_modules')); fs.writeFileSync(path.join(tmpDir, 'backend', '.env'), 'PORT=3001\n'); fs.writeFileSync(path.join(tmpDir, 'backend', 'db-schema.sql'), 'CREATE TABLE test();'); // No frontend node_modules, no frontend .env, no frontend build process.chdir(tmpDir); const state = detectInfraState(); expect(state.backendNodeModules).toBe(true); expect(state.frontendNodeModules).toBe(false); expect(state.backendEnvExists).toBe(true); expect(state.frontendEnvExists).toBe(false); expect(state.frontendBuildExists).toBe(false); expect(state.schemaFileExists).toBe(true); expect(state.sqliteDbExists).toBe(false); // npmAvailable should be true in test environment expect(typeof state.npmAvailable).toBe('boolean'); expect(typeof state.dockerAvailable).toBe('boolean'); expect(typeof state.psqlAvailable).toBe('boolean'); expect(typeof state.postgresRunning).toBe('boolean'); }); test('detectInfraState detects SQLite database when present', () => { fs.mkdirSync(path.join(tmpDir, 'backend')); fs.mkdirSync(path.join(tmpDir, 'frontend')); fs.writeFileSync(path.join(tmpDir, 'backend', 'cve_database.db'), ''); process.chdir(tmpDir); const state = detectInfraState(); expect(state.sqliteDbExists).toBe(true); }); test('detectInfraState detects frontend build when present', () => { fs.mkdirSync(path.join(tmpDir, 'backend')); fs.mkdirSync(path.join(tmpDir, 'frontend')); fs.mkdirSync(path.join(tmpDir, 'frontend', 'build'), { recursive: true }); fs.writeFileSync(path.join(tmpDir, 'frontend', 'build', 'index.html'), ''); process.chdir(tmpDir); const state = detectInfraState(); expect(state.frontendBuildExists).toBe(true); }); }); // ───────────────────────────────────────────────────────────────────────────── // Test 7: Frontend build skip on unchanged REACT_APP_* values // ───────────────────────────────────────────────────────────────────────────── describe('Frontend build skip on unchanged REACT_APP_* values', () => { test('shouldSkipFrontendBuild returns true when REACT_APP_* values are identical', () => { const oldEnv = new Map([ ['REACT_APP_API_BASE', 'http://localhost:3001/api'], ['REACT_APP_API_HOST', 'http://localhost:3001'], ]); const newEnv = new Map([ ['REACT_APP_API_BASE', 'http://localhost:3001/api'], ['REACT_APP_API_HOST', 'http://localhost:3001'], ]); expect(shouldSkipFrontendBuild(oldEnv, newEnv)).toBe(true); }); test('shouldSkipFrontendBuild returns false when REACT_APP_* values differ', () => { const oldEnv = new Map([ ['REACT_APP_API_BASE', 'http://localhost:3001/api'], ['REACT_APP_API_HOST', 'http://localhost:3001'], ]); const newEnv = new Map([ ['REACT_APP_API_BASE', 'http://192.168.1.100:4000/api'], ['REACT_APP_API_HOST', 'http://192.168.1.100:4000'], ]); expect(shouldSkipFrontendBuild(oldEnv, newEnv)).toBe(false); }); test('shouldSkipFrontendBuild returns false when oldFrontendEnv is null', () => { const newEnv = new Map([ ['REACT_APP_API_BASE', 'http://localhost:3001/api'], ['REACT_APP_API_HOST', 'http://localhost:3001'], ]); expect(shouldSkipFrontendBuild(null, newEnv)).toBe(false); }); test('shouldSkipFrontendBuild returns false when one REACT_APP_* key differs', () => { const oldEnv = new Map([ ['REACT_APP_API_BASE', 'http://localhost:3001/api'], ['REACT_APP_API_HOST', 'http://localhost:3001'], ]); const newEnv = new Map([ ['REACT_APP_API_BASE', 'http://localhost:3001/api'], ['REACT_APP_API_HOST', 'http://newhost:3001'], ]); expect(shouldSkipFrontendBuild(oldEnv, newEnv)).toBe(false); }); }); // ───────────────────────────────────────────────────────────────────────────── // Test 8: Node.js version check // ───────────────────────────────────────────────────────────────────────────── describe('Node.js version check', () => { let mockExit; beforeEach(() => { mockExit = jest.spyOn(process, 'exit').mockImplementation(() => { throw new Error('process.exit called'); }); }); afterEach(() => { mockExit.mockRestore(); }); test('checkNodeVersion does not exit on current Node.js version (>= 18)', () => { // Current test environment should be Node 18+ expect(() => checkNodeVersion()).not.toThrow(); }); test('checkNodeVersion would exit on Node < 18 (simulated via version override)', () => { const originalVersion = process.version; Object.defineProperty(process, 'version', { value: 'v16.20.0', writable: true }); try { expect(() => checkNodeVersion()).toThrow('process.exit called'); expect(mockExit).toHaveBeenCalledWith(1); } finally { Object.defineProperty(process, 'version', { value: originalVersion, writable: true }); } }); }); // ───────────────────────────────────────────────────────────────────────────── // Test 9: parseDockerCompose with real docker-compose.yml // ───────────────────────────────────────────────────────────────────────────── describe('parseDockerCompose with real docker-compose.yml', () => { test('correctly parses the project actual docker-compose.yml', () => { const composePath = path.join(__dirname, '..', '..', 'docker-compose.yml'); const result = parseDockerCompose(composePath); expect(result).not.toBeNull(); expect(result.user).toBe('steam'); expect(result.password).toBe('sV4xmC9xAUCFop0ypxMVS056QgPqGrX'); expect(result.database).toBe('cve_dashboard'); expect(result.port).toBe('5433'); }); test('parseDockerCompose returns null for non-existent file', () => { const result = parseDockerCompose('/nonexistent/docker-compose.yml'); expect(result).toBeNull(); }); test('parseDockerCompose returns null for invalid YAML content', () => { const tmpDir = createTempDir(); const filePath = path.join(tmpDir, 'docker-compose.yml'); fs.writeFileSync(filePath, 'this is not valid yaml at all\nno services here\n'); const result = parseDockerCompose(filePath); expect(result).toBeNull(); removeTempDir(tmpDir); }); test('parseDockerCompose handles compose file with shell variable defaults', () => { const tmpDir = createTempDir(); const filePath = path.join(tmpDir, 'docker-compose.yml'); const content = [ 'services:', ' postgres:', ' image: postgres:16-alpine', ' environment:', ' POSTGRES_DB: testdb', ' POSTGRES_USER: testuser', ' POSTGRES_PASSWORD: ${PG_PASS:-mysecretpass}', ' ports:', ' - "5434:5432"', ].join('\n'); fs.writeFileSync(filePath, content, 'utf8'); const result = parseDockerCompose(filePath); expect(result).not.toBeNull(); expect(result.user).toBe('testuser'); expect(result.password).toBe('mysecretpass'); expect(result.database).toBe('testdb'); expect(result.port).toBe('5434'); removeTempDir(tmpDir); }); }); // ───────────────────────────────────────────────────────────────────────────── // Test 10: parseEnvFile round-trip — write and re-read produces identical values // ───────────────────────────────────────────────────────────────────────────── describe('parseEnvFile round-trip', () => { let tmpDir; beforeEach(() => { tmpDir = createTempDir(); }); afterEach(() => { removeTempDir(tmpDir); }); test('writing and re-reading produces identical managed values', () => { const values = new Map(); values.set('PORT', '3001'); values.set('API_HOST', 'localhost'); values.set('CORS_ORIGINS', 'http://localhost:3000'); values.set('DATABASE_URL', 'postgresql://steam:sV4xmC9xAUCFop0ypxMVS056QgPqGrX@localhost:5433/cve_dashboard'); values.set('SESSION_SECRET', 'my-session-secret-at-least-16-chars'); const backendDescriptors = VARIABLE_DESCRIPTORS.filter(d => d.target === 'backend'); const content = generateEnvContent(values, GROUP_ORDER, backendDescriptors, []); const filePath = path.join(tmpDir, '.env'); writeEnvFile(filePath, content); const parsed = parseEnvFile(filePath); // All values we set should be recovered for (const [key, value] of values) { const descriptor = VARIABLE_DESCRIPTORS.find(d => d.name === key); if (descriptor && descriptor.target === 'backend') { expect(parsed.managed.get(key)).toBe(value); } } }); test('round-trip preserves values with special characters', () => { const values = new Map(); values.set('PORT', '3001'); values.set('API_HOST', 'localhost'); values.set('CORS_ORIGINS', 'http://localhost:3000'); values.set('DATABASE_URL', 'postgresql://user:p@ss$word@localhost:5433/db'); values.set('SESSION_SECRET', 'secret with spaces and #hash and $dollar'); const backendDescriptors = VARIABLE_DESCRIPTORS.filter(d => d.target === 'backend'); const content = generateEnvContent(values, GROUP_ORDER, backendDescriptors, []); const filePath = path.join(tmpDir, '.env'); writeEnvFile(filePath, content); const parsed = parseEnvFile(filePath); expect(parsed.managed.get('PORT')).toBe('3001'); expect(parsed.managed.get('API_HOST')).toBe('localhost'); // Values with special chars are quoted, parseEnvFile strips quotes expect(parsed.managed.get('SESSION_SECRET')).toBe('secret with spaces and #hash and $dollar'); }); }); // ───────────────────────────────────────────────────────────────────────────── // Test 11: generateEnvContent with all groups — complete output format // ───────────────────────────────────────────────────────────────────────────── describe('generateEnvContent with all groups', () => { test('produces complete output with all group headers and values', () => { const values = new Map(); // Core Settings values.set('PORT', '3001'); values.set('API_HOST', 'localhost'); values.set('CORS_ORIGINS', 'http://localhost:3000'); // Database values.set('DATABASE_URL', 'postgresql://steam:pass@localhost:5433/cve_dashboard'); // Session values.set('SESSION_SECRET', 'a-very-long-secret-key-here-1234'); // NVD API values.set('NVD_API_KEY', 'nvd-key-123'); // Ivanti values.set('IVANTI_API_KEY', 'ivanti-key-456'); values.set('IVANTI_CLIENT_ID', '1550'); // Atlas values.set('ATLAS_API_URL', 'https://atlas.example.com'); values.set('ATLAS_API_USER', 'atlasuser'); values.set('ATLAS_API_PASS', 'atlaspass'); // Jira values.set('JIRA_BASE_URL', 'https://jira.example.com'); values.set('JIRA_AUTH_METHOD', 'basic'); values.set('JIRA_API_USER', 'jirauser'); values.set('JIRA_API_TOKEN', 'jira-token-789'); // CARD values.set('CARD_API_URL', 'https://card.example.com'); values.set('CARD_API_USER', 'carduser'); values.set('CARD_API_PASS', 'cardpass'); // GitLab values.set('GITLAB_URL', 'http://steam-gitlab.charterlab.com'); values.set('GITLAB_PROJECT_ID', '42'); values.set('GITLAB_PAT', 'glpat-abc123'); const backendDescriptors = VARIABLE_DESCRIPTORS.filter(d => d.target === 'backend'); const content = generateEnvContent(values, GROUP_ORDER, backendDescriptors, []); // Verify all group headers present expect(content).toContain('# --- Core Settings ---'); expect(content).toContain('# --- Database ---'); expect(content).toContain('# --- Session ---'); expect(content).toContain('# --- NVD API ---'); expect(content).toContain('# --- Ivanti Integration ---'); expect(content).toContain('# --- Atlas Integration ---'); expect(content).toContain('# --- Jira Integration ---'); expect(content).toContain('# --- CARD Integration ---'); expect(content).toContain('# --- GitLab Integration ---'); // Verify values present expect(content).toContain('PORT=3001'); expect(content).toContain('NVD_API_KEY=nvd-key-123'); expect(content).toContain('IVANTI_CLIENT_ID=1550'); expect(content).toContain('GITLAB_PROJECT_ID=42'); // Verify LF line endings (no \r) expect(content).not.toContain('\r'); // Verify trailing newline expect(content.endsWith('\n')).toBe(true); }); }); // ───────────────────────────────────────────────────────────────────────────── // Test 12: generateEnvContent with skipped groups — excluded from output // ───────────────────────────────────────────────────────────────────────────── describe('generateEnvContent with skipped groups', () => { test('skipped groups produce no KEY=value lines in output', () => { const values = new Map(); // Only set required group values values.set('PORT', '3001'); values.set('API_HOST', 'localhost'); values.set('CORS_ORIGINS', 'http://localhost:3000'); values.set('DATABASE_URL', 'postgresql://steam:pass@localhost:5433/cve_dashboard'); values.set('SESSION_SECRET', 'a-very-long-secret-key-here-1234'); const backendDescriptors = VARIABLE_DESCRIPTORS.filter(d => d.target === 'backend'); const content = generateEnvContent(values, GROUP_ORDER, backendDescriptors, []); // Verify no optional group variables appear const optionalVarNames = VARIABLE_DESCRIPTORS .filter(d => OPTIONAL_GROUPS.includes(d.group)) .map(d => d.name); for (const varName of optionalVarNames) { // Should not have any KEY= line for these variables const regex = new RegExp(`^${varName}=`, 'm'); expect(content).not.toMatch(regex); } }); test('partial optional groups — only configured groups appear', () => { const values = new Map(); values.set('PORT', '3001'); values.set('API_HOST', 'localhost'); values.set('CORS_ORIGINS', 'http://localhost:3000'); values.set('DATABASE_URL', 'postgresql://steam:pass@localhost:5433/cve_dashboard'); values.set('SESSION_SECRET', 'a-very-long-secret-key-here-1234'); // Only configure Jira values.set('JIRA_BASE_URL', 'https://jira.example.com'); values.set('JIRA_API_USER', 'user'); values.set('JIRA_API_TOKEN', 'token-value-here'); const backendDescriptors = VARIABLE_DESCRIPTORS.filter(d => d.target === 'backend'); const content = generateEnvContent(values, GROUP_ORDER, backendDescriptors, []); expect(content).toContain('# --- Jira Integration ---'); expect(content).toContain('JIRA_BASE_URL=https://jira.example.com'); // Other optional groups should not appear expect(content).not.toContain('# --- NVD API ---'); expect(content).not.toContain('# --- Ivanti Integration ---'); expect(content).not.toContain('# --- Atlas Integration ---'); expect(content).not.toContain('# --- CARD Integration ---'); expect(content).not.toContain('# --- GitLab Integration ---'); }); }); // ───────────────────────────────────────────────────────────────────────────── // Test 13: createBackup — backup file creation with timestamp naming // ───────────────────────────────────────────────────────────────────────────── describe('createBackup', () => { let tmpDir; beforeEach(() => { tmpDir = createTempDir(); }); afterEach(() => { removeTempDir(tmpDir); }); test('creates backup file with timestamp naming', () => { const originalPath = path.join(tmpDir, '.env'); fs.writeFileSync(originalPath, 'PORT=3001\nAPI_HOST=localhost\n'); const backupPath = createBackup(originalPath); expect(fs.existsSync(backupPath)).toBe(true); // Backup should match pattern: .env.backup.YYYYMMDD_HHmmss expect(backupPath).toMatch(/\.env\.backup\.\d{8}_\d{6}$/); // Content should be identical const originalContent = fs.readFileSync(originalPath, 'utf8'); const backupContent = fs.readFileSync(backupPath, 'utf8'); expect(backupContent).toBe(originalContent); }); test('creates numbered backup when timestamp backup already exists', () => { const originalPath = path.join(tmpDir, '.env'); fs.writeFileSync(originalPath, 'PORT=3001\n'); // Create first backup const firstBackup = createBackup(originalPath); expect(fs.existsSync(firstBackup)).toBe(true); // Modify original fs.writeFileSync(originalPath, 'PORT=4000\n'); // Create second backup — since timestamp is same second, it should use .bak.N // We simulate by creating the expected timestamp backup manually const now = new Date(); const timestamp = now.getFullYear().toString() + String(now.getMonth() + 1).padStart(2, '0') + String(now.getDate()).padStart(2, '0') + '_' + String(now.getHours()).padStart(2, '0') + String(now.getMinutes()).padStart(2, '0') + String(now.getSeconds()).padStart(2, '0'); const expectedTimestampPath = `${originalPath}.backup.${timestamp}`; // If the timestamp backup already exists (from first call), second call uses .bak.N if (fs.existsSync(expectedTimestampPath)) { const secondBackup = createBackup(originalPath); expect(secondBackup).toMatch(/\.bak\.\d+$/); expect(fs.existsSync(secondBackup)).toBe(true); const content = fs.readFileSync(secondBackup, 'utf8'); expect(content).toBe('PORT=4000\n'); } }); test('backup preserves file content exactly', () => { const originalPath = path.join(tmpDir, '.env'); const content = '# --- Core Settings ---\nPORT=3001\nAPI_HOST=localhost\n\n# Custom\nMY_VAR=hello\n'; fs.writeFileSync(originalPath, content); const backupPath = createBackup(originalPath); const backupContent = fs.readFileSync(backupPath, 'utf8'); expect(backupContent).toBe(content); }); });