mirror of
https://github.com/Cacti/cacti.git
synced 2026-10-23 03:27:02 +09:00
test: add unit tests for layout.js and realtime.js helpers (#7911)
This commit is contained in:
1 parent
b7b3fd8808
commit
063a385a94
3 files changed
+194
No files matched your search
@@ -0,0 +1 @@
|
||||
Add unit tests for layout.js and realtime.js helper functions
|
||||
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
+-------------------------------------------------------------------------+
|
||||
| Copyright (C) 2004-2026 The Cacti Group |
|
||||
+-------------------------------------------------------------------------+
|
||||
| Cacti: The Complete RRDtool-based Graphing Solution |
|
||||
+-------------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const test = require('node:test');
|
||||
const vm = require('node:vm');
|
||||
|
||||
const layoutSource = fs.readFileSync(
|
||||
path.join(__dirname, '..', '..', 'include', 'layout.js'),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
// Pull a single function's source out of layout.js by brace-matching, so the
|
||||
// helper can be exercised in isolation without a browser or the full bundle.
|
||||
function extractFunction(name) {
|
||||
const start = layoutSource.indexOf(`function ${name}(`);
|
||||
assert.notEqual(start, -1, `${name}() must exist in layout.js`);
|
||||
|
||||
const bodyStart = layoutSource.indexOf('{', start);
|
||||
let depth = 0;
|
||||
|
||||
for (let offset = bodyStart; offset < layoutSource.length; offset++) {
|
||||
if (layoutSource[offset] === '{') {
|
||||
depth++;
|
||||
} else if (layoutSource[offset] === '}') {
|
||||
depth--;
|
||||
|
||||
if (depth === 0) {
|
||||
return layoutSource.slice(start, offset + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Unable to extract ${name}()`);
|
||||
}
|
||||
|
||||
function load(name, context = {}) {
|
||||
return vm.runInNewContext(`${extractFunction(name)}\n${name};`, context);
|
||||
}
|
||||
|
||||
test('escapeString neutralises the HTML metacharacters used for XSS', () => {
|
||||
const escapeString = load('escapeString');
|
||||
|
||||
assert.equal(escapeString('<script>'), '<script>');
|
||||
assert.equal(escapeString(`"'` + '`'), '"'`');
|
||||
assert.equal(escapeString('a & b'), 'a & b'); // ampersand is intentionally left alone
|
||||
assert.equal(escapeString('plain'), 'plain');
|
||||
assert.equal(escapeString(42), '42'); // coerces non-strings
|
||||
});
|
||||
|
||||
test('basename returns the file portion of a path', () => {
|
||||
const basename = load('basename');
|
||||
|
||||
assert.equal(basename('/usr/local/graph_view.php'), 'graph_view.php');
|
||||
assert.equal(basename('graph_view.php?action=edit'), 'graph_view.php');
|
||||
assert.equal(basename('/x/y/report.php', '.php'), 'report');
|
||||
assert.equal(basename('graphs.php'), 'graphs.php');
|
||||
assert.equal(basename('/trailing/'), 'index.php'); // empty result falls back
|
||||
});
|
||||
|
||||
test('getTreeSearchWidth keeps the search box within bounds', () => {
|
||||
const getTreeSearchWidth = load('getTreeSearchWidth');
|
||||
|
||||
assert.equal(getTreeSearchWidth(200), 130); // navWidth - 70
|
||||
assert.equal(getTreeSearchWidth(100), 40); // clamped to the 40 floor
|
||||
assert.equal(getTreeSearchWidth(50), 40);
|
||||
});
|
||||
|
||||
test('getTimestampFromDate converts a MySQL datetime to epoch seconds', () => {
|
||||
const getTimestampFromDate = load('getTimestampFromDate', { Date });
|
||||
|
||||
const expected = new Date(2020, 0, 15, 13, 30).getTime() / 1000;
|
||||
assert.equal(getTimestampFromDate('2020-01-15 13:30:00'), expected);
|
||||
assert.equal(getTimestampFromDate(undefined), '');
|
||||
});
|
||||
|
||||
test('getQueryString reads a named parameter from the query string', () => {
|
||||
const getQueryString = load('getQueryString', {
|
||||
window: { location: { search: '?action=edit&id=42&q=a+b' } },
|
||||
decodeURIComponent,
|
||||
RegExp,
|
||||
});
|
||||
|
||||
assert.equal(getQueryString('action'), 'edit');
|
||||
assert.equal(getQueryString('id'), '42');
|
||||
assert.equal(getQueryString('q'), 'a b'); // + decodes to space
|
||||
assert.equal(getQueryString('missing'), null);
|
||||
});
|
||||
|
||||
test('base64_encode round-trips UTF-8 text through btoa', () => {
|
||||
const base64_encode = load('base64_encode', { btoa, unescape, encodeURIComponent });
|
||||
|
||||
assert.equal(base64_encode('Cacti'), 'Q2FjdGk=');
|
||||
assert.equal(base64_encode(''), '');
|
||||
// multi-byte characters survive the encodeURIComponent/unescape dance
|
||||
assert.equal(base64_encode('café'), Buffer.from('café', 'utf8').toString('base64'));
|
||||
});
|
||||
|
||||
test('countHiddenCols counts the header cells hidden with display:none', () => {
|
||||
function jqueryFor(displays) {
|
||||
const cells = displays.map((display) => ({ display }));
|
||||
|
||||
return function $(arg) {
|
||||
if (arg && typeof arg.display === 'string') {
|
||||
return { css: (prop) => (prop === 'display' ? arg.display : undefined) };
|
||||
}
|
||||
|
||||
return {
|
||||
find: () => ({
|
||||
each: (callback) => cells.forEach((cell) => callback.call(cell)),
|
||||
}),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const countHiddenCols = load('countHiddenCols', { $: jqueryFor(['none', 'table-cell', 'none', 'block']) });
|
||||
|
||||
assert.equal(countHiddenCols('#atable'), 2);
|
||||
assert.equal(load('countHiddenCols', { $: jqueryFor([]) })('#empty'), 0);
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
+-------------------------------------------------------------------------+
|
||||
| Copyright (C) 2004-2026 The Cacti Group |
|
||||
+-------------------------------------------------------------------------+
|
||||
| Cacti: The Complete RRDtool-based Graphing Solution |
|
||||
+-------------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const test = require('node:test');
|
||||
const vm = require('node:vm');
|
||||
|
||||
const realtimeSource = fs.readFileSync(
|
||||
path.join(__dirname, '..', '..', 'include', 'realtime.js'),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
function extractFunction(name) {
|
||||
const start = realtimeSource.indexOf(`function ${name}(`);
|
||||
assert.notEqual(start, -1, `${name}() must exist in realtime.js`);
|
||||
|
||||
const bodyStart = realtimeSource.indexOf('{', start);
|
||||
let depth = 0;
|
||||
|
||||
for (let offset = bodyStart; offset < realtimeSource.length; offset++) {
|
||||
if (realtimeSource[offset] === '{') {
|
||||
depth++;
|
||||
} else if (realtimeSource[offset] === '}') {
|
||||
depth--;
|
||||
|
||||
if (depth === 0) {
|
||||
return realtimeSource.slice(start, offset + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Unable to extract ${name}()`);
|
||||
}
|
||||
|
||||
function load(name, context = {}) {
|
||||
return vm.runInNewContext(`${extractFunction(name)}\n${name};`, context);
|
||||
}
|
||||
|
||||
test('realtimeDetectBrowser classifies the common user agents', () => {
|
||||
const detect = (ua) => load('realtimeDetectBrowser', { navigator: { userAgent: ua } })();
|
||||
|
||||
assert.equal(detect('Mozilla/5.0 (compatible; MSIE 10.0)'), 'IE');
|
||||
assert.equal(detect('Mozilla/5.0 (X11) Chrome/120 Safari/537'), 'Chrome');
|
||||
assert.equal(detect('Mozilla/5.0 (X11; Linux) Gecko Firefox/121'), 'FF');
|
||||
assert.equal(detect('curl/8.0'), 'Other');
|
||||
});
|
||||
|
||||
test('countRealtimeGraphs counts only the active entries', () => {
|
||||
const countRealtimeGraphs = load('countRealtimeGraphs', {
|
||||
realtimeArray: { g1: true, g2: false, g3: true, g4: true },
|
||||
});
|
||||
|
||||
assert.equal(countRealtimeGraphs(), 3);
|
||||
assert.equal(load('countRealtimeGraphs', { realtimeArray: {} })(), 0);
|
||||
});
|
||||
Reference in new issue
Block a user