import assert from 'node:assert/strict' import test from 'node:test' import { readFile, writeFile, mkdir, mkdtemp, stat, rm } from 'node:fs/promises' import path from 'node:path' import os from 'node:os' import { execFile } from 'node:child_process' import { promisify } from 'node:util' import { fileURLToPath } from 'node:url' import { agentBackendUrl, agentSetupReady, renderAgentInstaller } from '../../../frontend/utils/agentInstaller.mjs' const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') const bundle = JSON.parse(await readFile(path.resolve(root, '../../frontend/assets/device-agent/bundle.json'), 'utf8')) const agentId = 'b48c0e33-1abd-4b11-89fa-1bb7b39a3057' const token = 'fedeo_agent_' + 'a'.repeat(64) const options = { agentId, token, backendUrl: 'https://fedeo.example.com/backend', autostart: true } const execute = promisify(execFile) const hasSystemdAnalyze = process.platform === 'linux' && await execute('systemd-analyze', ['--version']) .then(() => true, error => { if (error.code === 'ENOENT') return false; throw error }) test('backend URLs preserve reverse-proxy paths and reject credentials or non-HTTP protocols', () => { assert.equal(agentBackendUrl('/backend/', 'https://fedeo.example.com'), options.backendUrl) assert.equal(agentBackendUrl('', 'http://localhost:3100'), 'http://localhost:3100') assert.throws(() => agentBackendUrl('javascript:alert(1)')) assert.throws(() => agentBackendUrl('https://user:secret@example.com')) assert.throws(() => agentBackendUrl('https://example.com?token=secret')) }) test('installer validates tokens and identities before generating executable content', () => { assert.throws(() => renderAgentInstaller(bundle, { ...options, token: 'not-a-token' })) assert.throws(() => renderAgentInstaller(bundle, { ...options, agentId: "x'; touch /tmp/unwanted; '" })) assert.throws(() => renderAgentInstaller(bundle, { ...options, backendUrl: 'https://example.com/\nFEDEO_AGENT_TOKEN=bad' })) const script = renderAgentInstaller(bundle, options) assert.ok(!script.includes('@@')) }) test('connection readiness requires an active agent and a fresh heartbeat for this setup', () => { const now = Date.now() assert.equal(agentSetupReady({ active: true, lastSeenAt: new Date(now).toISOString() }, now - 1000, now), true) assert.equal(agentSetupReady({ active: false, lastSeenAt: new Date(now).toISOString() }, 0, now), false) assert.equal(agentSetupReady({ active: true, lastSeenAt: new Date(now - 5000).toISOString() }, now - 1000, now), false) assert.equal(agentSetupReady({ active: true, lastSeenAt: 'invalid' }), false) }) for (const start of [false, true]) { test('downloaded installer writes runtime and credentials; start=' + start, { skip: process.platform !== 'linux' }, async (t) => { const temp = await mkdtemp(path.join(os.tmpdir(), 'fedeo-installer-test-')) t.after(() => rm(temp, { recursive: true, force: true })) const bin = path.join(temp, 'bin') await mkdir(bin) // No package manager, scanner or real systemd command may run during this test. await writeFile(path.join(bin, 'sudo'), '#!/bin/sh\nexit 99\n', { mode: 0o755 }) await writeFile(path.join(bin, 'scanimage'), '#!/bin/sh\nexit 0\n', { mode: 0o755 }) await writeFile(path.join(bin, 'id'), '#!/bin/sh\nprintf "1000\\n"\n', { mode: 0o755 }) await writeFile(path.join(bin, 'systemctl'), '#!/bin/sh\nprintf "%s\\n" "$*" >> "$FEDEO_TEST_SYSTEMD_LOG"\n', { mode: 0o755 }) const realPython = (await execute('sh', ['-c', 'command -v python3'])).stdout.trim() await writeFile(path.join(bin, 'python3'), '#!/bin/sh\nif [ "$1" = "-c" ] && [ "$2" = "import PIL, cv2, numpy" ]; then exit 0; fi\nexec "$FEDEO_TEST_PYTHON" "$@"\n', { mode: 0o755 }) await writeFile(path.join(bin, 'tesseract'), '#!/bin/sh\nprintf "deu\\neng\\n"\n', { mode: 0o755 }) const env = { ...process.env, PATH: bin + ':' + process.env.PATH, XDG_DATA_HOME: path.join(temp, 'data with spaces %'), XDG_CONFIG_HOME: path.join(temp, 'config'), XDG_STATE_HOME: path.join(temp, 'state'), FEDEO_TEST_SYSTEMD_LOG: path.join(temp, 'systemd.log'), FEDEO_TEST_PYTHON: realPython, } const scriptPath = path.join(temp, 'install.sh') await writeFile(scriptPath, renderAgentInstaller(bundle, options)) await execute('bash', ['-n', scriptPath]) const { stdout: output } = await execute('bash', [scriptPath, ...(start ? [] : ['--no-start'])], { env, encoding: 'utf8' }) const agentDir = path.join(env.XDG_DATA_HOME, 'fedeo-device-agent', agentId) const configPath = path.join(env.XDG_CONFIG_HOME, 'fedeo-device-agent', agentId + '.env') const config = await readFile(configPath, 'utf8') assert.ok(config.includes('FEDEO_URL=' + options.backendUrl + '\n')) assert.ok(config.includes('FEDEO_AGENT_TOKEN=' + token + '\n')) assert.equal((await stat(configPath)).mode & 0o777, 0o600) assert.ok(!output.includes(token)) for (const [file, encoded] of Object.entries(bundle.files)) { assert.equal((await readFile(path.join(agentDir, file))).toString('base64'), encoded) } const unitPath = path.join(env.XDG_CONFIG_HOME, 'systemd/user', 'fedeo-device-agent-' + agentId + '.service') const unit = await readFile(unitPath, 'utf8') assert.ok(unit.includes('data with spaces %%')) assert.ok(!unit.includes(token)) await t.test('systemd accepts the generated service, including a working directory with spaces and percent signs', { skip: !hasSystemdAnalyze }, async () => { await execute('systemd-analyze', ['--user', 'verify', unitPath]) }) const calls = await readFile(env.FEDEO_TEST_SYSTEMD_LOG, 'utf8') assert.equal(calls.includes('--user enable fedeo-device-agent-'), start) assert.equal(calls.includes('--user restart fedeo-device-agent-'), start) assert.equal(calls.includes('--user stop fedeo-device-agent-'), start) }) }