Files
CyberSim-OS/tests/run_tests.js
T

277 lines
12 KiB
JavaScript

/**
* 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 = '<script>alert("pwned")</script><img src=x onerror=alert(1)>';
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('<script>'));
assert(ctx['learner.firstName'].includes('&lt;script&gt;'));
assert(ctx['learner.firstName'].includes('&lt;img src=x onerror=alert(1)&gt;'));
});
console.log('\n--- 4. Scenario Validation & Spanish Localization Overlay Tests ---');
const nexacorePath = path.join(srcDir, 'scenarios/nexacore-orientation/scenario.json');
const nexacoreEsPath = path.join(srcDir, 'scenarios/nexacore-orientation/locales/es.json');
const quickstartPath = path.join(srcDir, 'scenarios/quickstart-example/scenario.json');
const quickstartEsPath = path.join(srcDir, 'scenarios/quickstart-example/locales/es.json');
const nexacoreData = JSON.parse(fs.readFileSync(nexacorePath, 'utf8'));
const nexacoreEsData = JSON.parse(fs.readFileSync(nexacoreEsPath, 'utf8'));
const quickstartData = JSON.parse(fs.readFileSync(quickstartPath, 'utf8'));
const quickstartEsData = JSON.parse(fs.readFileSync(quickstartEsPath, 'utf8'));
await test('NexaCore scenario schema validates cleanly with supportedLocales and login metadata', async () => {
const res = validateSchema(nexacoreData);
assert.equal(res.valid, true, `Errors: ${JSON.stringify(res.errors)}`);
assert.deepEqual(nexacoreData.supportedLocales, ['en', 'es']);
assert(nexacoreData.login.networkName);
});
await test('Quickstart scenario schema validates cleanly with supportedLocales and login metadata', async () => {
const res = validateSchema(quickstartData);
assert.equal(res.valid, true, `Errors: ${JSON.stringify(res.errors)}`);
assert.deepEqual(quickstartData.supportedLocales, ['en', 'es']);
assert(quickstartData.login.networkName);
});
await test('Spanish translation overlay does not alter scenario structure, IDs, or scoring rules', async () => {
assert(nexacoreEsData.title);
assert(nexacoreEsData.messages.email_welcome.subject);
assert(quickstartEsData.messages.email_welcome.body.includes('{{learner.firstName}}'));
});
console.log('\n--- 5. Deterministic Scenario Fingerprinting & Certificate Verification Tests ---');
let fingerprintEn = '';
let fingerprintEs = '';
await test('Scenario fingerprint calculation is deterministic', async () => {
fingerprintEn = await calculateScenarioFingerprint(nexacoreData);
assert.equal(typeof fingerprintEn, 'string');
assert.equal(fingerprintEn.length, 64);
});
await test('Scenario fingerprint remains 100% identical when loaded with Spanish locale overlay', async () => {
const localizedScenario = JSON.parse(JSON.stringify(nexacoreData));
localizedScenario._canonicalScenario = nexacoreData;
localizedScenario.title = nexacoreEsData.title; // Overlay title
fingerprintEs = await calculateScenarioFingerprint(localizedScenario);
assert.equal(fingerprintEs, fingerprintEn, 'Fingerprints must match exactly across all locales!');
});
let generatedCert = null;
await test('CertificateGenerator creates valid structured *.cybercert with canonical integrity hash', async () => {
const mockScoreResult = {
totalScore: 90,
isPassed: true,
passingThreshold: 80,
categories: {
'threat-detection': { score: 30, max: 30, label: 'Threat Detection' },
'safe-handling': { score: 30, max: 40, label: 'Safe Handling' },
'policy-adherence': { score: 30, max: 30, label: 'Policy Adherence' }
}
};
const gen = new CertificateGenerator(nexacoreData, mockScoreResult, fingerprintEn);
generatedCert = await gen.createCertificateData('Maria Garcia');
assert.equal(generatedCert.schema_version, 1);
assert.equal(generatedCert.learner.name, 'Maria Garcia');
assert.equal(generatedCert.scenario_fingerprint, fingerprintEn);
assert.equal(generatedCert.evaluation.score, 90);
assert.equal(generatedCert.evaluation.passed, true);
assert(generatedCert.integrity_hash);
});
await test('CertificateVerifier verifies genuine certificate successfully offline', async () => {
const verifyResult = await CertificateVerifier.verify(generatedCert);
assert.equal(verifyResult.valid, true);
assert.equal(verifyResult.passed, true);
});
await test('CertificateVerifier rejects tampered score or modified learner name', async () => {
const tamperedCert = JSON.parse(JSON.stringify(generatedCert));
tamperedCert.evaluation.score = 100; // Alter score without updating integrity hash
const verifyResult = await CertificateVerifier.verify(tamperedCert);
assert.equal(verifyResult.valid, false);
});
console.log('\n--- 6. Offline & Zero External Dependencies Integrity Test ---');
await test('Codebase contains zero remote external CDN or runtime internet dependencies', async () => {
const scanDir = (dir) => {
const files = fs.readdirSync(dir, { withFileTypes: true });
for (const f of files) {
const fullPath = path.join(dir, f.name);
if (f.isDirectory()) {
scanDir(fullPath);
} else if (/\.(html|js|css)$/.test(f.name)) {
const content = fs.readFileSync(fullPath, 'utf8');
// Disallow external CDNs like cdnjs, unpkg, google fonts, etc.
assert(!content.includes('fonts.googleapis.com'), `Google Fonts detected in ${f.name}`);
assert(!content.includes('cdnjs.cloudflare.com'), `CDNJS detected in ${f.name}`);
assert(!content.includes('unpkg.com'), `Unpkg detected in ${f.name}`);
assert(!content.includes('cdn.jsdelivr.net'), `jsDelivr detected in ${f.name}`);
}
}
};
scanDir(srcDir);
});
console.log('\n============================================================');
console.log(` Tests Passed: ${passedTests} / ${totalTests}`);
console.log('============================================================\n');
if (passedTests !== totalTests) {
process.exit(1);
}
}
runAllTests();