/** * CyberSim OS - Automated Test Suite (Phase 2.5) * * Runs headless with Node.js to verify localization, branding, login validation, * personalization safety, deterministic fingerprinting, and offline integrity. */ import assert from 'node:assert/strict'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const rootDir = path.resolve(__dirname, '..'); const srcDir = path.resolve(rootDir, 'src'); // Polyfill Web Crypto for Node test environment if needed if (!globalThis.crypto) { const cryptoModule = await import('node:crypto'); globalThis.crypto = cryptoModule.webcrypto; } // Import modules import { I18nService } from '../src/js/core/i18n.js'; import { BrandingManager } from '../src/js/core/branding.js'; import { ActionDispatcher } from '../src/js/engine/action_dispatcher.js'; import { calculateScenarioFingerprint } from '../src/js/scenario/fingerprint.js'; import { validateSchema } from '../src/js/scenario/schema.js'; import { validateScenario } from '../src/js/scenario/validator.js'; import { CertificateGenerator } from '../src/js/cert/cert_generator.js'; import { CertificateVerifier } from '../src/js/cert/cert_verifier.js'; let passedTests = 0; let totalTests = 0; async function test(name, fn) { totalTests++; try { await fn(); console.log(` ✓ ${name}`); passedTests++; } catch (err) { console.error(` ✕ ${name}`); console.error(err); } } async function runAllTests() { console.log('\n============================================================'); console.log(' CyberSim OS Phase 2.5 - Automated Test Suite'); console.log('============================================================\n'); console.log('--- 1. Localization Engine Tests ---'); const enCatalog = JSON.parse(fs.readFileSync(path.join(srcDir, 'locales/en.json'), 'utf8')); const esCatalog = JSON.parse(fs.readFileSync(path.join(srcDir, 'locales/es.json'), 'utf8')); const i18n = new I18nService({ defaultLocale: 'en', enabledLocales: ['en', 'es'] }); i18n.setDictionary('en', enCatalog); i18n.setDictionary('es', esCatalog); await test('Default locale loads English strings', async () => { assert.equal(i18n.t('login.welcome'), 'Welcome'); assert.equal(i18n.t('apps.inlook'), 'Inlook Mail'); }); await test('Alternate locale loads Spanish demonstration strings', async () => { await i18n.setLocale('es'); assert.equal(i18n.t('login.welcome'), 'Bienvenido'); assert.equal(i18n.t('apps.inlook'), 'Correo Inlook'); }); await test('Missing translation falls back to default and English deterministically', async () => { const testI18n = new I18nService({ defaultLocale: 'en', enabledLocales: ['en', 'es'] }); testI18n.setDictionary('en', { 'only.in.en': 'English Text', 'common.key': 'EN Common' }); testI18n.setDictionary('es', { 'only.in.es': 'Spanish Text' }); await testI18n.setLocale('es'); // Key in es -> returns es assert.equal(testI18n.t('only.in.es'), 'Spanish Text'); // Key missing in es but present in en -> falls back to en assert.equal(testI18n.t('only.in.en'), 'English Text'); // Completely nonexistent key shows visible indicator assert.equal(testI18n.t('nonexistent.key'), '[missing: nonexistent.key]'); }); await test('Localization string interpolation replaces parameters correctly', async () => { await i18n.setLocale('en'); const res = i18n.t('cert.statement', { scenarioTitle: 'NexaCore' }); assert(res.includes('NexaCore')); }); await test('Default embedded English catalog contains all cert.* keys without missing labels', async () => { const rawI18n = new I18nService({ defaultLocale: 'en' }); const certTitle = rawI18n.t('cert.title'); const compTitle = rawI18n.t('cert.competencyTitle'); const env = rawI18n.t('cert.environment'); assert(!certTitle.startsWith('[missing:')); assert(!compTitle.startsWith('[missing:')); assert(!env.startsWith('[missing:')); }); console.log('\n--- 2. Corporate Branding Tests ---'); const customConfig = { organization: { name: 'Acme Security Corp', shortName: 'Acme', accentColor: '#315A78', supportName: 'Acme Helpdesk' } }; await test('Configured corporate branding loads correctly', async () => { const branding = new BrandingManager(customConfig); const org = branding.getOrganization(); assert.equal(org.name, 'Acme Security Corp'); assert.equal(org.shortName, 'Acme'); assert.equal(org.accentColor, '#315A78'); }); await test('Missing branding fields fall back to CyberSim defaults', async () => { const branding = new BrandingManager({}); const org = branding.getOrganization(); assert.equal(org.name, 'CyberSim Enterprise'); assert.equal(org.shortName, 'CyberSim'); assert.equal(org.accentColor, '#2563eb'); }); console.log('\n--- 3. Personalization & Safe Template Interpolation Tests ---'); await test('ActionDispatcher resolves {{learner.firstName}}, {{learner.lastName}}, and {{learner.name}}', async () => { const mockScenario = { learner: { firstName: 'Chris', lastName: 'Morgan', name: 'Chris Morgan', role: 'Specialist' }, organizations: [{ name: 'NexaCore' }] }; const dispatcher = new ActionDispatcher({ scenario: mockScenario }); const ctx = dispatcher._buildInterpolationContext(); assert.equal(ctx['learner.firstName'], 'Chris'); assert.equal(ctx['learner.lastName'], 'Morgan'); assert.equal(ctx['learner.name'], 'Chris Morgan'); const tmpl = 'Hello {{learner.firstName}} {{learner.lastName}}, full: {{learner.name}}.'; const interpolated = dispatcher.interpolate(tmpl, ctx); assert.equal(interpolated, 'Hello Chris Morgan, full: Chris Morgan.'); }); await test('Malicious HTML entered as first/last name is escaped harmlessly', async () => { const xssPayload = ''; const mockScenario = { learner: { firstName: xssPayload, lastName: xssPayload, name: xssPayload }, organizations: [{ name: 'NexaCore' }] }; const dispatcher = new ActionDispatcher({ scenario: mockScenario }); const ctx = dispatcher._buildInterpolationContext(); assert(!ctx['learner.firstName'].includes('