You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
102 lines
4.1 KiB
JavaScript
102 lines
4.1 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const test = require('node:test');
|
|
|
|
const root = path.join(__dirname, '..');
|
|
|
|
function read(relativePath) {
|
|
return fs.readFileSync(path.join(root, relativePath), 'utf8');
|
|
}
|
|
|
|
test('dashboard starts live polling and exposes required controls', () => {
|
|
const html = read('frontend/index.html');
|
|
const dashboard = read('frontend/dashboard.js');
|
|
|
|
assert.match(html, /id="history-interval"/);
|
|
assert.match(html, /name="enabled-sensor"/);
|
|
assert.match(html, /id="api-token-input"/);
|
|
assert.match(html, /id="history-retention-days"/);
|
|
assert.match(html, /id="notifications-enabled"/);
|
|
assert.match(html, /id="webhook-url"/);
|
|
assert.match(html, /id="telegram-bot-token"/);
|
|
assert.match(html, /id="alarm-history-list"/);
|
|
assert.match(html, /id="alarm-threshold-controls"/);
|
|
assert.match(dashboard, /updateDashboard\(\);/);
|
|
assert.match(dashboard, /updateEnabledSensors/);
|
|
assert.match(dashboard, /\/api\/config\/runtime/);
|
|
assert.match(dashboard, /\/api\/alarms/);
|
|
assert.match(dashboard, /saveAlarmThresholds/);
|
|
assert.match(dashboard, /\/api\/config\/alarms/);
|
|
assert.match(dashboard, /updateHistoryRetention/);
|
|
assert.match(dashboard, /saveNotificationConfig/);
|
|
assert.match(dashboard, /\/api\/config\/notifications/);
|
|
assert.match(dashboard, /getApiHeaders/);
|
|
assert.match(dashboard, /filterPointsForCurrentDay/);
|
|
assert.match(
|
|
dashboard,
|
|
/setInterval\(updateDashboard,\s*POLLING_INTERVAL_MS\)/
|
|
);
|
|
});
|
|
|
|
test('api protects critical endpoints with token and rate limiting hooks', () => {
|
|
const server = read('api/server.js');
|
|
const notifications = read('api/notification-service.js');
|
|
|
|
assert.match(server, /API_RATE_LIMIT_WINDOW_MS/);
|
|
assert.match(server, /API_RATE_LIMIT_MAX/);
|
|
assert.match(server, /rateLimitCriticalApi/);
|
|
assert.match(server, /Retry-After/);
|
|
assert.match(server, /timingSafeEqual/);
|
|
assert.match(server, /\/api\/config\/notifications/);
|
|
assert.match(notifications, /redactNotificationConfig/);
|
|
assert.match(notifications, /sendTelegramNotification/);
|
|
assert.match(notifications, /sendWebhookNotification/);
|
|
});
|
|
|
|
test('dashboard HTML has balanced structural containers', () => {
|
|
const html = read('frontend/index.html');
|
|
const openDivs = (html.match(/<div\b/g) || []).length;
|
|
const closeDivs = (html.match(/<\/div>/g) || []).length;
|
|
const openSections = (html.match(/<section\b/g) || []).length;
|
|
const closeSections = (html.match(/<\/section>/g) || []).length;
|
|
|
|
assert.equal(openDivs, closeDivs);
|
|
assert.equal(openSections, closeSections);
|
|
});
|
|
|
|
test('terminal output does not append untrusted HTML', () => {
|
|
const service = read('frontend/ezo-service.js');
|
|
|
|
assert.doesNotMatch(service, /terminalOutput\.innerHTML/);
|
|
assert.match(service, /line\.textContent = message/);
|
|
assert.match(service, /window\.getApiHeaders/);
|
|
});
|
|
|
|
test('sensor Makefiles reference their real implementation objects', () => {
|
|
assert.match(read('sensors/EZOPH/Makefile'), /OBJ=main\.o ezoph\.o/);
|
|
assert.match(read('sensors/EZODO/Makefile'), /OBJ=main\.o ezodo\.o/);
|
|
assert.match(read('sensors/EZOEC/Makefile'), /OBJ=main\.o ezoec\.o/);
|
|
assert.ok(read('sensors/EZODO/main.c').trim().length > 0);
|
|
});
|
|
|
|
test('dashboard uses locally installed chart and spreadsheet libraries', () => {
|
|
const html = read('frontend/index.html');
|
|
|
|
assert.match(html, /src="\/vendor\/chart\.js"/);
|
|
assert.match(html, /src="\/vendor\/xlsx\.js"/);
|
|
assert.doesNotMatch(html, /cdn\.jsdelivr|cdn\.sheetjs/);
|
|
});
|
|
|
|
test('hardware acquisition batches all EZO addresses under the shared lock', () => {
|
|
const source = read('sensors/EZOCommand/acquire.c');
|
|
const acquisitionService = read('api/acquisition-service.js');
|
|
|
|
for (const address of ['0x61', '0x63', '0x64', '0x66']) {
|
|
assert.match(source, new RegExp(address));
|
|
}
|
|
assert.match(source, /photobioreactor-i2c\.lock/);
|
|
assert.match(source, /EZO_ACQUIRE \/dev\/i2c-1 \[temperature\|ph\|do\|ec\]/);
|
|
assert.match(acquisitionService, /EZO_ENABLED_SENSORS/);
|
|
});
|