Initial
This commit is contained in:
159
server/test/job-output.test.mjs
Normal file
159
server/test/job-output.test.mjs
Normal file
@@ -0,0 +1,159 @@
|
||||
import { db, load, makeUser } from './setup.mjs';
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
|
||||
const { createHost } = await load('../models/hostModel.js');
|
||||
const { createCredential } = await load('../models/credentialModel.js');
|
||||
const { updateSettings } = await load('../models/settingsModel.js');
|
||||
const { generateJobOutputs } = await load('../services/jobOutputService.js');
|
||||
const { getJobOutputFile, listJobOutputs, listJobOutputsForJob } = await load('../models/jobOutputModel.js');
|
||||
const { executeScriptTest } = await load('../services/powershellRunner.js');
|
||||
|
||||
const owner = makeUser({ email: 'job-output-owner@test.local' });
|
||||
const stranger = makeUser({ email: 'job-output-stranger@test.local' });
|
||||
const ts = new Date().toISOString();
|
||||
|
||||
function addLog(jobId, hostId, stream, line, offset = 0) {
|
||||
db.prepare(`
|
||||
INSERT INTO job_logs (id, job_id, host_id, stream, line, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`).run(`log_output_${jobId}_${hostId}_${stream}_${offset}`, jobId, hostId, stream, line, new Date(Date.parse(ts) + offset).toISOString());
|
||||
}
|
||||
|
||||
test('job output generation writes stdout PDF, datatable JSON, and terminal transcript artifacts', async () => {
|
||||
const scriptId = 'scr_job_output_test';
|
||||
const jobId = 'job_output_test';
|
||||
db.prepare(`INSERT INTO scripts (id, name, content, visibility, owner_user_id, version, created_at, updated_at)
|
||||
VALUES (?, 'Inventory Table.ps1', 'Get-Process', 'personal', ?, 1, ?, ?)`).run(scriptId, owner, ts, ts);
|
||||
const hostA = createHost({ name: 'Output Host A', address: 'host-a.local', transport: 'local', osFamily: 'linux', visibility: 'personal' }, owner);
|
||||
const hostB = createHost({ name: 'Output Host B', address: 'host-b.local', transport: 'local', osFamily: 'linux', visibility: 'personal' }, owner);
|
||||
db.prepare(`INSERT INTO jobs (id, runplan_id, script_id, status, triggered_by, started_at, ended_at, created_at)
|
||||
VALUES (?, NULL, ?, 'completed', ?, ?, ?, ?)`).run(jobId, scriptId, owner, ts, ts, ts);
|
||||
db.prepare(`INSERT INTO job_hosts (id, job_id, host_id, status, exit_code, started_at, ended_at)
|
||||
VALUES (?, ?, ?, 'completed', 0, ?, ?)`).run('jhost_output_a', jobId, hostA.id, ts, ts);
|
||||
db.prepare(`INSERT INTO job_hosts (id, job_id, host_id, status, exit_code, started_at, ended_at)
|
||||
VALUES (?, ?, ?, 'completed', 0, ?, ?)`).run('jhost_output_b', jobId, hostB.id, ts, ts);
|
||||
addLog(jobId, hostA.id, 'stdout', 'Name,Status', 1);
|
||||
addLog(jobId, hostA.id, 'stdout', 'Spooler,Running', 2);
|
||||
addLog(jobId, hostB.id, 'stdout', 'Name,Status', 3);
|
||||
addLog(jobId, hostB.id, 'stdout', 'WinRM,Running', 4);
|
||||
addLog(jobId, hostB.id, 'stderr', 'Ignored by stdout PDF', 5);
|
||||
|
||||
await generateJobOutputs(jobId);
|
||||
const outputs = listJobOutputsForJob(jobId, owner, false);
|
||||
const kinds = outputs.map((output) => output.kind).sort();
|
||||
|
||||
assert.deepEqual(kinds, ['datatable', 'stdout-pdf', 'terminal']);
|
||||
assert.equal(listJobOutputs(stranger, false).some((output) => output.jobId === jobId), false);
|
||||
|
||||
const tableOutput = outputs.find((output) => output.kind === 'datatable');
|
||||
const tableFile = getJobOutputFile(tableOutput.id, owner, false);
|
||||
const table = JSON.parse(fs.readFileSync(tableFile.filePath, 'utf8'));
|
||||
assert.deepEqual(table.columns.map((column) => column.key), ['Host', 'HostStatus', 'Name', 'Status']);
|
||||
assert.equal(table.rows.length, 2);
|
||||
assert.ok(table.rows.some((row) => row.Host === 'Output Host A' && row.HostStatus === 'completed' && row.Name === 'Spooler' && row.Status === 'Running'));
|
||||
assert.ok(table.rows.some((row) => row.Host === 'Output Host B' && row.HostStatus === 'completed' && row.Name === 'WinRM' && row.Status === 'Running'));
|
||||
|
||||
const pdfOutput = outputs.find((output) => output.kind === 'stdout-pdf');
|
||||
const pdfFile = getJobOutputFile(pdfOutput.id, owner, false);
|
||||
assert.equal(pdfOutput.mimeType, 'application/pdf');
|
||||
assert.equal(fs.readFileSync(pdfFile.filePath).subarray(0, 4).toString(), '%PDF');
|
||||
|
||||
const terminalOutput = outputs.find((output) => output.kind === 'terminal');
|
||||
const terminalFile = getJobOutputFile(terminalOutput.id, owner, false);
|
||||
const transcript = fs.readFileSync(terminalFile.filePath, 'utf8');
|
||||
assert.match(transcript, /Ignored by stdout PDF/);
|
||||
});
|
||||
|
||||
test('script test runs generate the same FileLocker job output artifacts', async () => {
|
||||
const scriptOwner = makeUser({ email: 'job-output-script-test@test.local' });
|
||||
const scriptId = 'scr_job_output_script_test';
|
||||
db.prepare(`INSERT INTO scripts (id, name, content, visibility, owner_user_id, version, created_at, updated_at)
|
||||
VALUES (?, 'Script Test Output.ps1', 'Write-Output test', 'personal', ?, 1, ?, ?)`).run(scriptId, scriptOwner, ts, ts);
|
||||
const credential = createCredential({
|
||||
name: 'Output Test Credential',
|
||||
kind: 'username_password',
|
||||
username: 'TEST\\svc',
|
||||
secret: 'secret',
|
||||
visibility: 'personal'
|
||||
}, scriptOwner);
|
||||
const host = createHost({
|
||||
name: 'Output Test Host',
|
||||
address: 'output-test.local',
|
||||
transport: 'local',
|
||||
osFamily: 'linux',
|
||||
credentialId: credential.id,
|
||||
visibility: 'personal'
|
||||
}, scriptOwner);
|
||||
updateSettings({ allow_script_execution: 'false' });
|
||||
|
||||
const job = executeScriptTest(scriptId, { hostId: host.id, credentialId: credential.id }, scriptOwner);
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
const outputs = listJobOutputsForJob(job.id, scriptOwner, false);
|
||||
const kinds = outputs.map((output) => output.kind).sort();
|
||||
|
||||
assert.deepEqual(kinds, ['datatable', 'stdout-pdf', 'terminal']);
|
||||
updateSettings({ allow_script_execution: 'true' });
|
||||
});
|
||||
|
||||
test('job output datatable uses one last-stdout row per host for unstructured output', async () => {
|
||||
const scriptId = 'scr_job_output_last_line_test';
|
||||
const jobId = 'job_output_last_line_test';
|
||||
db.prepare(`INSERT INTO scripts (id, name, content, visibility, owner_user_id, version, created_at, updated_at)
|
||||
VALUES (?, 'Last Line.ps1', 'Write-Output lines', 'personal', ?, 1, ?, ?)`).run(scriptId, owner, ts, ts);
|
||||
const hostA = createHost({ name: 'Last Host A', address: 'last-a.local', transport: 'local', osFamily: 'linux', visibility: 'personal' }, owner);
|
||||
const hostB = createHost({ name: 'Last Host B', address: 'last-b.local', transport: 'local', osFamily: 'linux', visibility: 'personal' }, owner);
|
||||
db.prepare(`INSERT INTO jobs (id, runplan_id, script_id, status, triggered_by, started_at, ended_at, created_at)
|
||||
VALUES (?, NULL, ?, 'completed', ?, ?, ?, ?)`).run(jobId, scriptId, owner, ts, ts, ts);
|
||||
db.prepare(`INSERT INTO job_hosts (id, job_id, host_id, status, exit_code, started_at, ended_at)
|
||||
VALUES (?, ?, ?, 'completed', 0, ?, ?)`).run('jhost_last_a', jobId, hostA.id, ts, ts);
|
||||
db.prepare(`INSERT INTO job_hosts (id, job_id, host_id, status, exit_code, started_at, ended_at)
|
||||
VALUES (?, ?, ?, 'completed', 0, ?, ?)`).run('jhost_last_b', jobId, hostB.id, ts, ts);
|
||||
addLog(jobId, hostA.id, 'stdout', 'starting host A', 1);
|
||||
addLog(jobId, hostA.id, 'stdout', 'final host A', 2);
|
||||
addLog(jobId, hostB.id, 'stdout', 'starting host B', 3);
|
||||
addLog(jobId, hostB.id, 'stdout', '', 4);
|
||||
addLog(jobId, hostB.id, 'stdout', 'final host B', 5);
|
||||
|
||||
await generateJobOutputs(jobId);
|
||||
const tableOutput = listJobOutputsForJob(jobId, owner, false).find((output) => output.kind === 'datatable');
|
||||
const tableFile = getJobOutputFile(tableOutput.id, owner, false);
|
||||
const table = JSON.parse(fs.readFileSync(tableFile.filePath, 'utf8'));
|
||||
|
||||
assert.deepEqual(table.parsers, ['stdout-last-line']);
|
||||
assert.deepEqual(table.columns.map((column) => column.key), ['Host', 'HostStatus', 'Output']);
|
||||
assert.equal(table.rows.length, 2);
|
||||
assert.ok(table.rows.some((row) => row.Host === 'Last Host A' && row.Output === 'final host A'));
|
||||
assert.ok(table.rows.some((row) => row.Host === 'Last Host B' && row.Output === 'final host B'));
|
||||
assert.equal(table.rows.some((row) => row.Output === 'starting host A' || row.Output === 'starting host B'), false);
|
||||
});
|
||||
|
||||
test('job output datatable carries fields for structured object and array stdout', async () => {
|
||||
const scriptId = 'scr_job_output_structured_test';
|
||||
const jobId = 'job_output_structured_test';
|
||||
db.prepare(`INSERT INTO scripts (id, name, content, visibility, owner_user_id, version, created_at, updated_at)
|
||||
VALUES (?, 'Structured.ps1', 'ConvertTo-Json', 'personal', ?, 1, ?, ?)`).run(scriptId, owner, ts, ts);
|
||||
const hostA = createHost({ name: 'Structured Host A', address: 'structured-a.local', transport: 'local', osFamily: 'linux', visibility: 'personal' }, owner);
|
||||
const hostB = createHost({ name: 'Structured Host B', address: 'structured-b.local', transport: 'local', osFamily: 'linux', visibility: 'personal' }, owner);
|
||||
db.prepare(`INSERT INTO jobs (id, runplan_id, script_id, status, triggered_by, started_at, ended_at, created_at)
|
||||
VALUES (?, NULL, ?, 'completed', ?, ?, ?, ?)`).run(jobId, scriptId, owner, ts, ts, ts);
|
||||
db.prepare(`INSERT INTO job_hosts (id, job_id, host_id, status, exit_code, started_at, ended_at)
|
||||
VALUES (?, ?, ?, 'completed', 0, ?, ?)`).run('jhost_structured_a', jobId, hostA.id, ts, ts);
|
||||
db.prepare(`INSERT INTO job_hosts (id, job_id, host_id, status, exit_code, started_at, ended_at)
|
||||
VALUES (?, ?, ?, 'completed', 0, ?, ?)`).run('jhost_structured_b', jobId, hostB.id, ts, ts);
|
||||
addLog(jobId, hostA.id, 'stdout', '{"Name":"SvcA","Status":"Running","Version":1}', 1);
|
||||
addLog(jobId, hostB.id, 'stdout', '[{"Name":"SvcB","Status":"Running"},{"Name":"SvcC","Status":"Stopped"}]', 2);
|
||||
|
||||
await generateJobOutputs(jobId);
|
||||
const tableOutput = listJobOutputsForJob(jobId, owner, false).find((output) => output.kind === 'datatable');
|
||||
const tableFile = getJobOutputFile(tableOutput.id, owner, false);
|
||||
const table = JSON.parse(fs.readFileSync(tableFile.filePath, 'utf8'));
|
||||
|
||||
assert.deepEqual(table.parsers, ['json']);
|
||||
assert.deepEqual(table.columns.map((column) => column.key), ['Host', 'HostStatus', 'Name', 'Status', 'Version']);
|
||||
assert.equal(table.rows.length, 3);
|
||||
assert.ok(table.rows.some((row) => row.Host === 'Structured Host A' && row.Name === 'SvcA' && row.Version === '1'));
|
||||
assert.ok(table.rows.some((row) => row.Host === 'Structured Host B' && row.Name === 'SvcB' && row.Status === 'Running'));
|
||||
assert.ok(table.rows.some((row) => row.Host === 'Structured Host B' && row.Name === 'SvcC' && row.Status === 'Stopped'));
|
||||
});
|
||||
@@ -4,6 +4,10 @@ import assert from 'node:assert/strict';
|
||||
|
||||
const { listJobs, getJob } = await load('../models/jobModel.js');
|
||||
const { cancelSystemJob, listSystemJobs, runSystemJob } = await load('../services/systemJobService.js');
|
||||
const { createCredential } = await load('../models/credentialModel.js');
|
||||
const { createHost } = await load('../models/hostModel.js');
|
||||
const { updateSettings } = await load('../models/settingsModel.js');
|
||||
const { buildWrapper, executeScriptTest } = await load('../services/powershellRunner.js');
|
||||
|
||||
const owner = makeUser({ email: 'job-owner@test.local' });
|
||||
const stranger = makeUser({ email: 'job-stranger@test.local' });
|
||||
@@ -52,6 +56,68 @@ test('admins can see any job', () => {
|
||||
assert.ok(getJob(jobId, admin, true));
|
||||
});
|
||||
|
||||
test('executeScriptTest creates job host rows before runner starts', async () => {
|
||||
const scriptOwner = makeUser({ email: 'script-test-owner@test.local' });
|
||||
const localScriptId = 'scr_execute_test_1';
|
||||
db.prepare(`INSERT INTO scripts (id, name, content, visibility, owner_user_id, version, created_at, updated_at)
|
||||
VALUES (?, 'Script Execute Test', 'Write-Output test', 'personal', ?, 1, ?, ?)`).run(localScriptId, scriptOwner, ts, ts);
|
||||
const credential = createCredential({
|
||||
name: 'Script Test Credential',
|
||||
kind: 'username_password',
|
||||
username: 'TEST\\svc',
|
||||
secret: 'secret',
|
||||
visibility: 'personal'
|
||||
}, scriptOwner);
|
||||
const host = createHost({
|
||||
name: 'Script Test Host',
|
||||
address: 'script-test.local',
|
||||
fqdn: 'script-test.local',
|
||||
osFamily: 'windows',
|
||||
transport: 'winrm',
|
||||
credentialId: credential.id,
|
||||
visibility: 'personal',
|
||||
tags: []
|
||||
}, scriptOwner);
|
||||
updateSettings({ allow_script_execution: 'false' });
|
||||
|
||||
const job = executeScriptTest(localScriptId, { hostId: host.id, credentialId: credential.id }, scriptOwner);
|
||||
await new Promise((resolve) => setTimeout(resolve, 25));
|
||||
|
||||
const jobHost = db.prepare('SELECT * FROM job_hosts WHERE job_id = ? AND host_id = ?').get(job.id, host.id);
|
||||
const errorLog = db.prepare('SELECT * FROM job_logs WHERE job_id = ? AND stream = ? AND line LIKE ?').get(job.id, 'stderr', '%Script execution is disabled%');
|
||||
assert.equal(jobHost.status, 'failed');
|
||||
assert.ok(errorLog);
|
||||
updateSettings({ allow_script_execution: 'true' });
|
||||
});
|
||||
|
||||
test('PowerShell wrappers fail the host when Invoke-Command writes a terminating error', () => {
|
||||
const wrapper = buildWrapper('Write-Output "remote smoke"', {
|
||||
id: 'hst_wrapper_winrm',
|
||||
name: 'Wrapper WinRM',
|
||||
address: 'dallas.pucknet.local',
|
||||
transport: 'winrm'
|
||||
});
|
||||
|
||||
assert.match(wrapper, /\$ErrorActionPreference = "Stop"/);
|
||||
assert.match(wrapper, /Invoke-Command -ComputerName 'dallas\.pucknet\.local'.*-ErrorAction Stop/);
|
||||
assert.match(wrapper, /catch \{/);
|
||||
assert.match(wrapper, /exit 1/);
|
||||
assert.doesNotMatch(wrapper, /\$ErrorActionPreference = "Continue"/);
|
||||
});
|
||||
|
||||
test('local PowerShell wrappers propagate native command failures', () => {
|
||||
const wrapper = buildWrapper('Write-Output "local smoke"', {
|
||||
id: 'hst_wrapper_local',
|
||||
name: 'Wrapper Local',
|
||||
address: 'localhost',
|
||||
transport: 'local'
|
||||
});
|
||||
|
||||
assert.match(wrapper, /\$global:LASTEXITCODE/);
|
||||
assert.match(wrapper, /exit \$global:LASTEXITCODE/);
|
||||
assert.match(wrapper, /exit 0/);
|
||||
});
|
||||
|
||||
test('system jobs list execution and background jobs', () => {
|
||||
const listJobId = 'job_list_system_test_1';
|
||||
db.prepare(`INSERT INTO jobs (id, runplan_id, script_id, status, triggered_by, started_at, created_at)
|
||||
|
||||
89
server/test/runplan-schedules.test.mjs
Normal file
89
server/test/runplan-schedules.test.mjs
Normal file
@@ -0,0 +1,89 @@
|
||||
import { db, load, makeUser } from './setup.mjs';
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
const { createCredential } = await load('../models/credentialModel.js');
|
||||
const { createHost } = await load('../models/hostModel.js');
|
||||
const { updateSettings } = await load('../models/settingsModel.js');
|
||||
const {
|
||||
computeNextRun,
|
||||
createRunPlanSchedule,
|
||||
listDueRunPlanSchedules,
|
||||
listRunPlanScheduleRuns,
|
||||
previewScheduleOccurrences
|
||||
} = await load('../models/runPlanScheduleModel.js');
|
||||
const { dispatchDueRunPlanSchedules } = await load('../services/runPlanScheduleWorker.js');
|
||||
|
||||
const owner = makeUser({ email: 'schedule-owner@test.local' });
|
||||
const ts = new Date().toISOString();
|
||||
|
||||
function createRunnableRunPlan() {
|
||||
const scriptId = 'scr_schedule_test';
|
||||
db.prepare(`INSERT INTO scripts (id, name, content, visibility, owner_user_id, version, created_at, updated_at)
|
||||
VALUES (?, 'Scheduled Script.ps1', 'Write-Output scheduled', 'personal', ?, 1, ?, ?)`).run(scriptId, owner, ts, ts);
|
||||
const credential = createCredential({
|
||||
name: 'Schedule Credential',
|
||||
kind: 'username_password',
|
||||
username: 'TEST\\svc',
|
||||
secret: 'secret',
|
||||
visibility: 'personal'
|
||||
}, owner);
|
||||
const host = createHost({
|
||||
name: 'Schedule Host',
|
||||
address: 'schedule.local',
|
||||
transport: 'local',
|
||||
osFamily: 'linux',
|
||||
credentialId: credential.id,
|
||||
visibility: 'personal'
|
||||
}, owner);
|
||||
const runplanId = 'run_schedule_test';
|
||||
db.prepare(`INSERT INTO runplans (id, name, script_id, visibility, owner_user_id, parallel, created_at, updated_at)
|
||||
VALUES (?, 'Scheduled RunPlan', ?, 'personal', ?, 1, ?, ?)`).run(runplanId, scriptId, owner, ts, ts);
|
||||
db.prepare('INSERT INTO runplan_hosts (runplan_id, host_id) VALUES (?, ?)').run(runplanId, host.id);
|
||||
return runplanId;
|
||||
}
|
||||
|
||||
test('recurrence preview covers weekly and interval schedules', () => {
|
||||
const weekly = {
|
||||
scheduleType: 'weekly',
|
||||
startAt: '2026-06-01T14:00:00.000Z',
|
||||
timeOfDay: '09:30',
|
||||
daysOfWeek: [1, 3]
|
||||
};
|
||||
const preview = previewScheduleOccurrences(weekly, 3, new Date('2026-06-02T00:00:00.000Z'));
|
||||
assert.equal(preview.length, 3);
|
||||
assert.ok(preview.every((date) => [1, 3].includes(new Date(date).getDay())));
|
||||
|
||||
const intervalNext = computeNextRun({
|
||||
scheduleType: 'interval',
|
||||
startAt: '2026-06-01T00:00:00.000Z',
|
||||
intervalValue: 6,
|
||||
intervalUnit: 'hours'
|
||||
}, new Date('2026-06-01T07:00:00.000Z'));
|
||||
assert.equal(intervalNext, '2026-06-01T12:00:00.000Z');
|
||||
});
|
||||
|
||||
test('due RunPlan schedules dispatch jobs and record scheduler history', async () => {
|
||||
updateSettings({ allow_script_execution: 'false' });
|
||||
const runplanId = createRunnableRunPlan();
|
||||
const schedule = createRunPlanSchedule({
|
||||
name: 'Every minute schedule',
|
||||
runplanId,
|
||||
scheduleType: 'interval',
|
||||
startAt: '2026-01-01T00:00:00.000Z',
|
||||
intervalValue: 1,
|
||||
intervalUnit: 'minutes',
|
||||
visibility: 'personal'
|
||||
}, owner);
|
||||
db.prepare('UPDATE runplan_schedules SET next_run_at = ? WHERE id = ?').run('2026-01-01T00:02:00.000Z', schedule.id);
|
||||
|
||||
assert.ok(listDueRunPlanSchedules('2026-01-01T00:02:00.000Z').some((row) => row.id === schedule.id));
|
||||
const summary = await dispatchDueRunPlanSchedules({ nowIso: '2026-01-01T00:02:00.000Z' });
|
||||
|
||||
assert.equal(summary.dispatched, 1);
|
||||
const job = db.prepare('SELECT * FROM jobs WHERE runplan_id = ? ORDER BY created_at DESC').get(runplanId);
|
||||
assert.ok(job);
|
||||
const history = listRunPlanScheduleRuns(owner, false, 20);
|
||||
assert.ok(history.some((event) => event.scheduleId === schedule.id && event.jobId === job.id));
|
||||
updateSettings({ allow_script_execution: 'true' });
|
||||
});
|
||||
@@ -2,8 +2,9 @@ import { db, load } from './setup.mjs';
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
const { seed } = await load('../db.js');
|
||||
const { getSetting, getBooleanSetting, getStringSetting, updateSettings, effectiveTrustedOrigins } = await load('../models/settingsModel.js');
|
||||
const { resolvePowerShellExecutable, powerShellSpawnFailureLines } = await load('../services/powershellRunner.js');
|
||||
const { resolvePowerShellExecutable, powerShellSignalFailureLines, powerShellSpawnFailureLines } = await load('../services/powershellRunner.js');
|
||||
|
||||
test('unpinned settings seed as editable defaults', () => {
|
||||
// No SERVER_FQDN env var is set in the test environment, so server_fqdn is a
|
||||
@@ -37,6 +38,24 @@ test('powershell_bin config is used by the runner at execution time', () => {
|
||||
assert.equal(resolvePowerShellExecutable(), 'powershell.exe');
|
||||
});
|
||||
|
||||
test('powershell_bin can be edited even when older databases marked it env-sourced', () => {
|
||||
db.prepare('UPDATE settings SET value = ?, source = ? WHERE key = ?').run('pwsh', 'env', 'powershell_bin');
|
||||
const result = updateSettings({ powershell_bin: 'powershell' });
|
||||
|
||||
assert.equal(result.powershell_bin.value, 'powershell');
|
||||
assert.equal(result.powershell_bin.source, 'db');
|
||||
assert.equal(resolvePowerShellExecutable(), 'powershell');
|
||||
});
|
||||
|
||||
test('seed releases older env-sourced powershell_bin rows to the current default', () => {
|
||||
db.prepare('UPDATE settings SET value = ?, source = ? WHERE key = ?').run('old-pwsh', 'env', 'powershell_bin');
|
||||
seed();
|
||||
const row = db.prepare('SELECT value, source FROM settings WHERE key = ?').get('powershell_bin');
|
||||
|
||||
assert.equal(row.value, 'pwsh');
|
||||
assert.equal(row.source, 'default');
|
||||
});
|
||||
|
||||
test('PowerShell spawn failures include operator guidance', () => {
|
||||
const lines = powerShellSpawnFailureLines({ code: 'ENOENT', message: 'spawn pwsh ENOENT' }, 'pwsh');
|
||||
assert.ok(lines.some((line) => line.includes('configured PowerShell executable "pwsh"')));
|
||||
@@ -44,6 +63,13 @@ test('PowerShell spawn failures include operator guidance', () => {
|
||||
assert.ok(lines.some((line) => line.includes('powershell.exe')));
|
||||
});
|
||||
|
||||
test('PowerShell signal exits include crash diagnostics', () => {
|
||||
const lines = powerShellSignalFailureLines('SIGABRT', 'pwsh');
|
||||
assert.ok(lines.some((line) => line.includes('terminated by signal SIGABRT')));
|
||||
assert.ok(lines.some((line) => line.includes('PowerShell/.NET process aborted')));
|
||||
assert.ok(lines.some((line) => line.includes('pwsh -NoProfile')));
|
||||
});
|
||||
|
||||
test('effectiveTrustedOrigins merges admin edits with built-in defaults', () => {
|
||||
updateSettings({ trusted_origins: 'https://ops.example.com' });
|
||||
const origins = effectiveTrustedOrigins();
|
||||
|
||||
@@ -15,6 +15,7 @@ const {
|
||||
normalizeBaseUrl,
|
||||
normalizeVCenterVm,
|
||||
previewVCenterHosts,
|
||||
retrievePropertiesBody,
|
||||
settingsFromVCenterConnection,
|
||||
vmMatchesFilter
|
||||
} = {
|
||||
@@ -155,8 +156,22 @@ test('standalone VMware host connections use the Web Services profile', async ()
|
||||
assert.equal(preview.candidates[0].powerState, 'POWERED_ON');
|
||||
});
|
||||
|
||||
test('standalone VMware RetrievePropertiesEx SOAP closes boolean fields before paths', () => {
|
||||
const body = retrievePropertiesBody(
|
||||
{ type: 'PropertyCollector', value: 'ha-property-collector' },
|
||||
{ type: 'ContainerView', value: 'session-vm-view' },
|
||||
5
|
||||
);
|
||||
|
||||
assert.match(body, /<all>false<\/all><pathSet>name<\/pathSet>/);
|
||||
assert.match(body, /<skip>true<\/skip><selectSet/);
|
||||
assert.match(body, /<skip>false<\/skip><\/selectSet>/);
|
||||
assert.doesNotMatch(body, /<all>false><pathSet/);
|
||||
assert.doesNotMatch(body, /<skip>false><\/selectSet>/);
|
||||
});
|
||||
|
||||
test('standalone VMware VM candidates build managed host payloads', async () => {
|
||||
const { restore } = installStandaloneEsxiSoapStub();
|
||||
const { calls, restore } = installStandaloneEsxiSoapStub();
|
||||
const connection = {
|
||||
id: 'vc_host',
|
||||
name: 'ESXi 01',
|
||||
@@ -187,6 +202,10 @@ test('standalone VMware VM candidates build managed host payloads', async () =>
|
||||
assert.ok(payload.tags.includes('esxi-vm:vim.VirtualMachine:101'));
|
||||
assert.equal(power.checked, true);
|
||||
assert.equal(power.state, 'POWERED_ON');
|
||||
const powerBody = calls.filter((call) => call.body.includes('<RetrievePropertiesEx')).at(-1)?.body || '';
|
||||
assert.match(powerBody, /<all>false<\/all><pathSet>runtime\.powerState<\/pathSet>/);
|
||||
assert.match(powerBody, /<skip>false<\/skip><\/objectSet>/);
|
||||
assert.doesNotMatch(powerBody, /<all>false><pathSet/);
|
||||
});
|
||||
|
||||
test('standalone VMware SOAP retries alternate endpoint paths after /sdk 404', async () => {
|
||||
|
||||
Reference in New Issue
Block a user