first commit

This commit is contained in:
Mateusz Gruszczyński committed 2026-10-28 09:16:54 +01:00
commit 32a7ba8e8f
168 files changed
+123856

No files matched your search

+211
View File
@@ -0,0 +1,211 @@
/* Run with node --test web/tests/core.test.cjs. No packages required. */
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
function application(options = {}) {
const storage = () => ({ getItem: () => null, setItem() {}, removeItem() {} });
const context = {
window: {}, document: { querySelector: selector => selector === 'meta[name="allow-registration"]' && options.registration !== undefined ? { content: options.registration } : null }, sessionStorage: storage(), localStorage: storage(),
AbortController, URLSearchParams, Blob, URL, Response, fetch, setTimeout, clearTimeout,
requestAnimationFrame: fn => fn(), performance, console
};
context.window.window = context.window;
vm.createContext(context);
for (const name of ['i18n.js', 'core.js', 'views.js']) vm.runInContext(fs.readFileSync(path.join(__dirname, '../js', name), 'utf8'), context);
return context.window.MBS;
}
test('all four languages include every statically used translation key', () => {
const app = application();
const code = ['core.js', 'views.js', 'app.js'].map(file => fs.readFileSync(path.join(__dirname, '../js', file), 'utf8')).join('\n');
const keys = [...code.matchAll(/'((?:app|auth|common|confirm|dashboard|diffConfigs|files|footer|logs|nav|routers|settings|sidebar|toast|topbar|ui)\.[A-Za-z]+)'/g)].map(match => match[1]);
for (const lang of ['pl', 'en', 'es', 'no']) {
for (const key of keys.filter(key => key.includes('.'))) {
// Dynamic weekday suffix is validated by the schedule editor test.
if (key === 'settings.weekday') continue;
const value = key.split('.').reduce((node, part) => node && node[part], app.translations[lang]);
assert.equal(typeof value, 'string', `${lang}: ${key}`);
}
}
});
test('untrusted text and diff search results are HTML escaped', () => {
const app = application();
assert.equal(app.escape('<script a="1">&\''), '&lt;script a=&quot;1&quot;&gt;&amp;&#39;');
assert.equal(app.highlight('<img src=x onerror=alert(1)>', 'img'), '&lt;<mark>img</mark> src=x onerror=alert(1)&gt;');
app.state.diff = {
left_file_name: '<script>alert(1)</script>', right_file_name: '<img src=x>', diff_text: '<script>x</script>',
lines: [{ type: 'modified', left_number: 1, right_number: 1, left_text: '<img onerror=x>', right_text: '&password' }]
};
const html = app.diffResult();
assert.ok(!html.includes('<script>'));
assert.ok(!html.includes('<img'));
assert.ok(html.includes('&lt;img onerror=x&gt;'));
});
test('settings and device forms never prefill secret values returned by a legacy server', () => {
const app = application();
app.state.settingsTab = 'credentials';
const secret = 'do-not-render-secret-654321';
const settings = app.settingsForm({ global_ssh_key: secret, default_switchos_password: secret, smtp_password: secret, pushover_token: secret, pushover_userkey: secret });
const device = app.deviceForm({ id: 1, name: 'R1', device_type: 'routeros', host: '127.0.0.1', ssh_password: secret, ssh_key: secret });
assert.ok(!settings.includes(secret));
assert.ok(!device.includes(secret));
assert.ok(settings.includes('clear_global_ssh_key'));
assert.ok(device.includes('clear_ssh_password'));
});
test('registration is hidden unless the public page explicitly enables it', () => {
for (const registration of [undefined, 'false', 'TRUE', '1']) {
const app = application({ registration });
assert.equal(app.config.allowRegistration, false);
assert.ok(!app.authView(false).includes('href="/register"'));
assert.ok(!app.authView(true).includes('id="register-form"'));
}
const app = application({ registration: 'true' });
assert.equal(app.config.allowRegistration, true);
assert.ok(app.authView(false).includes('href="/register"'));
assert.ok(app.authView(true).includes('id="register-form"'));
});
test('login has no header or decorative SVGs and retains language and theme controls', () => {
const html = application().authView(false);
assert.ok(!html.includes('<header'));
assert.ok(!html.includes('<svg'));
assert.ok(html.includes('data-language'));
assert.ok(html.includes('data-action="theme"'));
});
test('account settings contain the existing password form outside the system settings form', async () => {
const app = application();
app.state.settingsTab = 'account';
app.api = async endpoint => endpoint.endsWith('scheduler-status') ? { jobs: [] } : {};
const html = await app.views.settings(false);
assert.ok(html.includes('data-settings-tab="account" role="tab" aria-selected="true"'));
assert.ok(html.includes('<section data-settings-pane="account">'));
assert.ok(html.includes('id="password-form"'));
assert.ok(html.indexOf('</form>') < html.indexOf('id="password-form"'));
assert.ok(html.includes('data-settings-system hidden'));
});
test('binary and SwitchOS backups never offer comparison actions or export tabs', () => {
const app = application();
const routeros = { id: 1, device_type: 'routeros', name: 'R1' };
const switchos = { id: 2, device_type: 'switchos', name: 'SW1', supports_export: false };
for (const file of [
{ id: 3, router_id: 1, backup_type: 'binary', device_type: 'routeros', file_name: 'test.backup' },
{ id: 4, router_id: 2, backup_type: 'binary', device_type: 'switchos', file_name: 'test.swb' },
{ id: 5, router_id: 2, backup_type: 'export', device_type: 'switchos', file_name: 'legacy.rsc' }
]) assert.ok(!app.backupTable([file]).includes('data-action="diff-latest"'));
app.state.cache.device = routeros;
app.state.detailTab = 'binary';
assert.ok(!app.detailBody().includes('data-action="compare-selected"'));
app.state.cache.device = switchos;
app.state.detailTab = 'overview';
assert.ok(!app.detailTabs().includes('data-detail-tab="export"'));
assert.ok(!app.detailBody().includes('data-action="device-export"'));
app.state.detailTab = 'policy';
assert.ok(!app.detailBody().includes('name="disable_export_backups"'));
app.state.cache.routers = [routeros, switchos];
app.state.cache.diffFiles = [
{ id: 1, backup_type: 'export', file_name: 'allowed.rsc', device_type: 'routeros' },
{ id: 2, backup_type: 'binary', file_name: 'forbidden.backup' },
{ id: 3, backup_type: 'export', file_name: 'forbidden.swb', device_type: 'switchos' }
];
const diff = app.diffControls();
assert.ok(diff.includes('allowed.rsc'));
assert.ok(!diff.includes('forbidden.'));
assert.ok(!diff.includes('SW1'));
});
test('SwitchOS credential forms preserve legacy choice and never prefill passwords', () => {
const app = application();
const base = { id: 2, device_type: 'switchos', name: 'SW1', ssh_user: 'own-admin', ssh_password: 'secret-not-to-render', has_own_password: true };
for (const mode of [null, true, false]) {
const html = app.deviceForm({ ...base, use_global_switchos_credentials: mode });
assert.ok(html.includes(`value="${mode === null ? '' : mode}" selected`));
assert.ok(!html.includes('secret-not-to-render'));
assert.equal(html.includes('id="device-own-credentials" hidden'), mode === true);
}
assert.equal(app.deviceUsername({ ssh_user: 'stored-own-admin', effective_username: null, use_global_switchos_credentials: true }), '—');
});
test('diff form errors never create an inline error element', () => {
const app = application();
let removed = false;
const form = { id: 'diff-form', querySelector: () => ({ remove() { removed = true; } }) };
app.formError(form, 'Choose two exports');
assert.equal(removed, true);
});
test('automation editors and active settings form contain no nested cards', () => {
const app = application();
app.state.settingsTab = 'automation';
const html = app.settingsForm({ export_cron: '0 2 * * mon', enable_auto_export: true });
assert.ok(html.includes('data-settings-pane="automation"'));
assert.ok(html.includes('class="automation-row"'));
assert.ok(!html.includes('class="card"'));
assert.ok(!html.includes('class="card-body"'));
assert.ok(!html.includes('schedule-editor'));
const status = app.schedulerTable({ jobs: [], timezone: 'Europe/Warsaw', running: true });
assert.ok(!status.includes('class="card'));
});
test('translated UI branding does not expose implementation terminology', () => {
const app = application();
for (const lang of ['pl', 'en', 'es', 'no']) assert.ok(!/rust|monolit/i.test(app.translations[lang].ui.about));
});
test('legacy naive UTC timestamps are parsed as UTC', () => {
const app = application();
assert.equal(app.parseDate('2026-01-02 03:04:05').toISOString(), '2026-01-02T03:04:05.000Z');
assert.equal(app.parseDate('2026-01-02T04:04:05+01:00').toISOString(), '2026-01-02T03:04:05.000Z');
assert.equal(app.parseDate('not a date'), null);
});
test('weekly schedules preserve actual APScheduler weekday meaning', () => {
const app = application();
const monday = app.scheduleEditor('export', 'settings.exportCron', '0 2 * * 0');
const tuesday = app.scheduleEditor('export', 'settings.exportCron', '0 2 * * 1');
const sunday = app.scheduleEditor('export', 'settings.exportCron', '0 2 * * sun');
assert.ok(monday.includes('value="mon" selected'));
assert.ok(tuesday.includes('value="tue" selected'));
assert.ok(sunday.includes('value="sun" selected'));
const custom = app.scheduleEditor('export', 'settings.exportCron', '*/15 * * * mon-fri');
assert.ok(custom.includes('value="custom" selected'));
});
test('dashboard renders only real input counts and escapes stored labels/logs', async () => {
const app = application();
app.api = async endpoint => endpoint === '/dashboard'
? { routers_count: 1, export_count: 0, binary_count: 0, total_backups: 0, storage: { total: 100, used: 30, free: 70, folder_used: 0 }, recent_logs: [{ message: '<script>bad</script>', timestamp: '2026-01-01' }] }
: endpoint === '/routers' ? [{ id: 1, name: 'one', last_connection_status: null }] : [];
const html = await app.views.dashboard();
assert.ok(html.includes('&lt;script&gt;bad&lt;/script&gt;'));
assert.ok(!html.includes('<script>bad'));
assert.ok(html.includes('stroke-dasharray="326.73 326.73"'));
const cards = html.split('<div class="cards-grid">')[1].split('<div class="grid-2">')[0];
assert.equal((cards.match(/class="metric"/g) || []).length, 4);
assert.ok(!cards.includes('<svg'));
assert.ok(!cards.includes('metric-icon'));
});
test('query parameters use URL encoding and skip empty values', () => {
const app = application();
const params = new URLSearchParams(app.backupQuery({ search: 'r1&router_id=2', router_id: '', order: 'asc' }));
assert.equal(params.get('search'), 'r1&router_id=2');
assert.equal(params.has('router_id'), false);
});
test('a late device response cannot overwrite the state after navigation', async () => {
const app = application(), pending = [];
app.api = endpoint => new Promise(resolve => pending.push({ endpoint, resolve }));
const load = app.views.device(1);
app.state.routeId += 1;
pending.forEach(({ endpoint, resolve }) => resolve(endpoint.startsWith('/routers') ? { id: 1, name: 'stale' } : []));
assert.equal(await load, '');
assert.equal(app.state.cache.device, undefined);
});