mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-06 23:35:08 +00:00
Whole-tree audit. Every symbol was checked against core, the plugin monorepo and all eight third-party plugins in plugins.json first. - Deprecate (removal 3.10.0) plugin-facing methods nothing calls: LogoDownloader bulk download, ConfigManager backup/secret wrappers, APIHelper extras, BackgroundDataService poll API, PluginManager / PluginStateManager info readers, and a few CacheManager, FontManager, BaseOddsManager, DynamicTeamResolver methods and PluginTestCase. plugin_api_usage.py learns their receiver names; DEPRECATIONS doc regenerated. - Remove core-internal dead code: CacheMetrics, Vegas status/stats plumbing, sync "new cycle" message (followers ignore unknown types), unused operation types, test-only PluginCatalog readers, IPC to_dict and ping, _parse_form_value, CacheStrategyProtocol, ErrorAggregator callbacks, duplicate web response helpers. - Web UI: drop never-mounted json-file-manager.js, the example widget, utils/error_handler.js, four uncalled PluginAPI methods, and 29 escapeHtml shims (call window.LEDEscape directly). Public globals, BaseWidget and widget names unchanged. - Remove six one-off scripts (owner decision) and the unused markupsafe and pytest-mock pins. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
111 lines
5.3 KiB
JavaScript
111 lines
5.3 KiB
JavaScript
// Verifies renderInstalledCards (extracted verbatim from plugins_manager.js):
|
|
// the two empty states, the card markup, and that it never publishes state.
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const V3 = path.resolve(__dirname, '../../../web_interface/static/v3');
|
|
const src = fs.readFileSync(V3 + '/plugins_manager.js', 'utf8');
|
|
|
|
function slice(fromMark, toMark) {
|
|
const a = src.indexOf(fromMark);
|
|
const b = src.indexOf(toMark, a + 1);
|
|
if (a < 0 || b < 0) { console.error('FAIL: cannot locate ' + fromMark); process.exit(1); }
|
|
return src.slice(a, b);
|
|
}
|
|
|
|
const container = {
|
|
innerHTML: '',
|
|
querySelectorAll: () => [], // no skeletons in this harness
|
|
};
|
|
// LEDEscape.html() escapes via a detached element, so mirror what a browser does
|
|
// when you read innerHTML back off textContent: & < > are escaped, quotes are not.
|
|
class FakeEl {
|
|
set textContent(v) { this._t = String(v == null ? '' : v); }
|
|
get textContent() { return this._t || ''; }
|
|
get innerHTML() {
|
|
return (this._t || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
}
|
|
}
|
|
global.document = {
|
|
getElementById: id => (id === 'installed-plugins-grid' ? container : null),
|
|
createElement: () => new FakeEl(),
|
|
};
|
|
global.window = global;
|
|
require('../led_escape').install(window);
|
|
global.pluginLog = () => {};
|
|
global.PLUGIN_DEBUG = false;
|
|
global.debugLog = () => {};
|
|
function setupInstalledEventDelegation() {} // stubbed; tested separately
|
|
|
|
eval(slice('function jsStringAttr(value)', '\nfunction isNewPlugin'));
|
|
eval(slice('function renderInstalledCards(plugins, total)',
|
|
'// Set up event delegation for plugin action buttons'));
|
|
|
|
let pass = 0, fail = 0;
|
|
const ok = (l, c, x) => c ? (pass++, console.log(' ok ' + l))
|
|
: (fail++, console.log(' FAIL ' + l + (x !== undefined ? ' → ' + JSON.stringify(x).slice(0, 300) : '')));
|
|
|
|
const SENTINEL = ['do', 'not', 'touch'];
|
|
window.installedPlugins = SENTINEL;
|
|
|
|
console.log('\n1. no plugins installed at all');
|
|
renderInstalledCards([], 0);
|
|
ok('shows "No plugins installed"', /No plugins installed/.test(container.innerHTML));
|
|
ok('does NOT show the filter empty state', !/No plugins match/.test(container.innerHTML));
|
|
ok('uses the plug icon', /fa-plug/.test(container.innerHTML));
|
|
|
|
console.log('\n2. plugins installed but none match the filters');
|
|
renderInstalledCards([], 12);
|
|
ok('shows "No plugins match your filters"', /No plugins match your filters/.test(container.innerHTML));
|
|
ok('does NOT claim nothing is installed', !/No plugins installed/.test(container.innerHTML));
|
|
ok('reports the installed total', /12 plugins installed/.test(container.innerHTML), container.innerHTML.match(/\d+ plugins? installed/));
|
|
ok('offers a Clear filters button', /data-action="clear-installed-filters"/.test(container.innerHTML));
|
|
ok('uses the filter icon', /fa-filter/.test(container.innerHTML));
|
|
|
|
console.log('\n3. singular/plural of the installed total');
|
|
renderInstalledCards([], 1);
|
|
ok('"1 plugin installed" (singular)', /1 plugin installed/.test(container.innerHTML) && !/1 plugins/.test(container.innerHTML));
|
|
|
|
console.log('\n4. real cards');
|
|
const plugins = [
|
|
{ id: 'alpha', name: 'Alpha', author: 'Ann', version: '1.0.0', category: 'weather',
|
|
description: 'Shows weather', enabled: true, tags: ['a', 'b'], verified: true },
|
|
{ id: 'beta', name: 'Beta', author: 'Bob', version: '1.0.0', latest_version: '2.0.0',
|
|
update_available: true, category: 'time', description: 'Clock', enabled: false },
|
|
];
|
|
renderInstalledCards(plugins, 5);
|
|
const html = container.innerHTML;
|
|
ok('renders 2 cards', (html.match(/class="plugin-card"/g) || []).length === 2);
|
|
ok('enabled card shows Enabled', /<span>Enabled<\/span>/.test(html));
|
|
ok('disabled card shows Disabled', /<span>Disabled<\/span>/.test(html));
|
|
ok('update badge shows target version', /v2\.0\.0 available/.test(html));
|
|
ok('update button labelled "Update to v2.0.0"', /Update to v2\.0\.0/.test(html));
|
|
ok('pulsing ring class on the outdated one', /plugin-update-available/.test(html));
|
|
ok('verified badge present', /Verified/.test(html));
|
|
ok('tags rendered', /badge-info">a</.test(html) && /badge-info">b</.test(html));
|
|
ok('per-plugin action hooks present',
|
|
/data-action="configure"/.test(html) && /data-action="update"/.test(html) &&
|
|
/data-action="uninstall"/.test(html) && /data-action="toggle"/.test(html));
|
|
ok('no empty state alongside cards', !/empty-state/.test(html));
|
|
|
|
console.log('\n5. it must not publish state');
|
|
ok('window.installedPlugins untouched', window.installedPlugins === SENTINEL, window.installedPlugins);
|
|
|
|
console.log('\n6. total defaults to list length when omitted');
|
|
renderInstalledCards([], undefined);
|
|
ok('omitted total + empty list → "no plugins installed"', /No plugins installed/.test(container.innerHTML));
|
|
|
|
console.log('\n7. XSS: hostile plugin metadata is escaped');
|
|
renderInstalledCards([{
|
|
id: 'evil', name: '<img src=x onerror=alert(1)>', author: '"><script>bad()</script>',
|
|
version: '1.0.0', category: 'x', description: '<b>nope</b>', enabled: true, tags: ['<i>t</i>'],
|
|
}], 1);
|
|
const evil = container.innerHTML;
|
|
ok('no raw <img', !/<img src=x/.test(evil));
|
|
ok('no raw <script', !/<script>bad/.test(evil));
|
|
ok('no raw <b> from description', !/<b>nope<\/b>/.test(evil));
|
|
ok('no raw <i> from tags', !/<i>t<\/i>/.test(evil));
|
|
ok('escaped entities present instead', /</.test(evil));
|
|
|
|
console.log(`\n${pass} passed, ${fail} failed\n`);
|
|
process.exit(fail ? 1 : 0);
|