import { db, makeUser, adminUserId, load } from './setup.mjs'; import test from 'node:test'; 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' }); const admin = adminUserId(); // Build a personal RunPlan owned by `owner` and a job they triggered, directly // in SQL so the test never invokes PowerShell. const ts = new Date().toISOString(); const scriptId = 'scr_job_test'; db.prepare(`INSERT INTO scripts (id, name, content, visibility, owner_user_id, version, created_at, updated_at) VALUES (?, 'Job Test', 'Write-Output 1', 'personal', ?, 1, ?, ?)`).run(scriptId, owner, ts, ts); const runplanId = 'run_job_test'; db.prepare(`INSERT INTO runplans (id, name, script_id, visibility, owner_user_id, parallel, created_at, updated_at) VALUES (?, 'Job Test Plan', ?, 'personal', ?, 1, ?, ?)`).run(runplanId, scriptId, owner, ts, ts); const jobId = 'job_test_1'; db.prepare(`INSERT INTO jobs (id, runplan_id, script_id, status, triggered_by, created_at) VALUES (?, ?, ?, 'completed', ?, ?)`).run(jobId, runplanId, scriptId, owner, ts); const scriptTestJobId = 'job_script_test_1'; db.prepare(`INSERT INTO jobs (id, runplan_id, script_id, status, triggered_by, created_at) VALUES (?, NULL, ?, 'failed', ?, ?)`).run(scriptTestJobId, scriptId, owner, ts); const runningJobId = 'job_cancel_test_1'; db.prepare(`INSERT INTO jobs (id, runplan_id, script_id, status, triggered_by, started_at, created_at) VALUES (?, ?, ?, 'running', ?, ?, ?)`).run(runningJobId, runplanId, scriptId, owner, ts, ts); db.prepare(`INSERT INTO job_hosts (id, job_id, host_id, status, started_at) VALUES ('jhost_cancel_test_1', ?, NULL, 'queued', NULL)`).run(runningJobId); test('the triggering operator can list and read their job', () => { assert.ok(listJobs(owner, false).some((j) => j.id === jobId)); assert.ok(getJob(jobId, owner, false)); }); test('script test jobs without a RunPlan stay visible to their triggering operator', () => { assert.ok(listJobs(owner, false).some((j) => j.id === scriptTestJobId)); const job = getJob(scriptTestJobId, owner, false); assert.equal(job.id, scriptTestJobId); assert.equal(job.runplanId, null); }); test('an unrelated user cannot see a personal job', () => { assert.ok(!listJobs(stranger, false).some((j) => j.id === jobId)); assert.equal(getJob(jobId, stranger, false), null); }); test('admins can see any job', () => { assert.ok(listJobs(admin, true).some((j) => j.id === jobId)); 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) VALUES (?, ?, ?, 'running', ?, ?, ?)`).run(listJobId, runplanId, scriptId, owner, ts, ts); const payload = listSystemJobs(); assert.ok(payload.jobs.some((job) => job.id === listJobId && job.canCancel)); assert.ok(payload.jobs.some((job) => job.id === 'worker:host-group-sync' && job.canRunNow)); }); test('cancelSystemJob marks running jobs canceled and audits the action', () => { const result = cancelSystemJob(runningJobId, admin, 'test cancellation'); assert.equal(result.ok, true); assert.equal(db.prepare('SELECT status FROM jobs WHERE id = ?').get(runningJobId).status, 'canceled'); assert.equal(db.prepare('SELECT status FROM job_hosts WHERE job_id = ?').get(runningJobId).status, 'canceled'); const action = db.prepare('SELECT * FROM system_job_actions WHERE target_id = ? AND action = ?').get(runningJobId, 'cancel'); assert.equal(action.status, 'success'); }); test('cancelSystemJob audits idle background worker cancellation attempts', () => { const result = cancelSystemJob('worker:host-group-sync', admin, 'not running'); assert.equal(result.ok, false); assert.match(result.error, /not running/i); const action = db.prepare('SELECT * FROM system_job_actions WHERE target_id = ? AND action = ? ORDER BY created_at DESC').get('worker:host-group-sync', 'cancel'); assert.equal(action.status, 'failed'); }); test('runSystemJob executes host group sync and audits run-now', async () => { const result = await runSystemJob('worker:host-group-sync', admin); assert.equal(result.ok, true); const action = db.prepare('SELECT * FROM system_job_actions WHERE target_id = ? AND action = ? ORDER BY created_at DESC').get('worker:host-group-sync', 'run-now'); assert.ok(action); });