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(//g) || []).length; const openSections = (html.match(//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/); });