92 lines
5.8 KiB
JavaScript
92 lines
5.8 KiB
JavaScript
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)
|
|
})
|
|
}
|