/**
* 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('