Initial
This commit is contained in:
@@ -3,6 +3,7 @@ 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 owner = makeUser({ email: 'job-owner@test.local' });
|
||||
const stranger = makeUser({ email: 'job-stranger@test.local' });
|
||||
@@ -23,6 +24,11 @@ db.prepare(`INSERT INTO jobs (id, runplan_id, script_id, status, triggered_by, c
|
||||
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));
|
||||
@@ -45,3 +51,36 @@ test('admins can see any job', () => {
|
||||
assert.ok(listJobs(admin, true).some((j) => j.id === jobId));
|
||||
assert.ok(getJob(jobId, admin, true));
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -6,7 +6,9 @@ import { load } from './setup.mjs';
|
||||
const {
|
||||
buildVmListPath,
|
||||
buildHostPayloadFromVm,
|
||||
buildStandaloneHostCandidate,
|
||||
filterSummaryVms,
|
||||
getVCenterVmPowerState,
|
||||
normalizeBaseUrl,
|
||||
normalizeVCenterVm,
|
||||
previewVCenterHosts,
|
||||
@@ -14,29 +16,124 @@ const {
|
||||
vmMatchesFilter
|
||||
} = await load('../services/vcenterService.js');
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
function soapResponse(body) {
|
||||
return new Response(`<?xml version="1.0" encoding="UTF-8"?><soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"><soapenv:Body>${body}</soapenv:Body></soapenv:Envelope>`, {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'text/xml' }
|
||||
});
|
||||
}
|
||||
|
||||
function installStandaloneEsxiSoapStub() {
|
||||
globalThis.fetch = async (_url, options = {}) => {
|
||||
const body = String(options.body || '');
|
||||
if (body.includes('<RetrieveServiceContent')) {
|
||||
return soapResponse(`<RetrieveServiceContentResponse xmlns="urn:vim25"><returnval><rootFolder type="Folder">ha-folder-root</rootFolder><propertyCollector type="PropertyCollector">ha-property-collector</propertyCollector><viewManager type="ViewManager">ViewManager</viewManager><sessionManager type="SessionManager">ha-sessionmgr</sessionManager></returnval></RetrieveServiceContentResponse>`);
|
||||
}
|
||||
if (body.includes('<Login')) {
|
||||
return soapResponse(`<LoginResponse xmlns="urn:vim25"><returnval type="UserSession">session-1</returnval></LoginResponse>`);
|
||||
}
|
||||
if (body.includes('<CreateContainerView')) {
|
||||
return soapResponse(`<CreateContainerViewResponse xmlns="urn:vim25"><returnval type="ContainerView">session[52b6]-vm-view</returnval></CreateContainerViewResponse>`);
|
||||
}
|
||||
if (body.includes('<RetrievePropertiesEx')) {
|
||||
return soapResponse(`<RetrievePropertiesExResponse xmlns="urn:vim25"><returnval><objects><obj type="VirtualMachine">vim.VirtualMachine:101</obj><propSet><name>name</name><val>ENG-ENT-APP01</val></propSet><propSet><name>guest.hostName</name><val>eng-ent-app01.contoso.local</val></propSet><propSet><name>guest.ipAddress</name><val>10.42.8.21</val></propSet><propSet><name>guest.guestFullName</name><val>Microsoft Windows Server 2022</val></propSet><propSet><name>runtime.powerState</name><val>poweredOn</val></propSet></objects></returnval></RetrievePropertiesExResponse>`);
|
||||
}
|
||||
return soapResponse('');
|
||||
};
|
||||
return () => { globalThis.fetch = originalFetch; };
|
||||
}
|
||||
|
||||
test('normalizeBaseUrl trims trailing slashes', () => {
|
||||
assert.equal(normalizeBaseUrl('https://vcenter.lab.local///'), 'https://vcenter.lab.local');
|
||||
});
|
||||
|
||||
test('standalone VMware host connections use the Web Services profile', async () => {
|
||||
const settings = settingsFromVCenterConnection({
|
||||
const restoreFetch = installStandaloneEsxiSoapStub();
|
||||
const connection = {
|
||||
id: 'vc_host',
|
||||
name: 'ESXi 01',
|
||||
target_type: 'host',
|
||||
base_url: 'https://esxi01.lab.local/sdk',
|
||||
username: 'root',
|
||||
password: 'secret',
|
||||
enabled: 1,
|
||||
api_mode: 'api'
|
||||
});
|
||||
};
|
||||
const settings = settingsFromVCenterConnection(connection);
|
||||
|
||||
assert.equal(settings.targetType, 'host');
|
||||
assert.equal(settings.apiMode, 'web-services');
|
||||
assert.equal(settings.baseUrl, 'https://esxi01.lab.local/sdk');
|
||||
|
||||
await assert.rejects(
|
||||
() => previewVCenterHosts({ connection: { ...settings, name: 'ESXi host' } }),
|
||||
/Standalone VMware host connections/
|
||||
const preview = await previewVCenterHosts({ connection, filter: { field: 'hostname', operator: 'contains', value: 'ENG-ENT' } });
|
||||
restoreFetch();
|
||||
|
||||
assert.equal(preview.count, 1);
|
||||
assert.equal(preview.diagnostics.standaloneHost, true);
|
||||
assert.equal(preview.candidates[0].source, 'standalone-vm');
|
||||
assert.equal(preview.candidates[0].fqdn, 'eng-ent-app01.contoso.local');
|
||||
assert.equal(preview.candidates[0].ipAddress, '10.42.8.21');
|
||||
assert.equal(preview.candidates[0].powerState, 'POWERED_ON');
|
||||
});
|
||||
|
||||
test('standalone VMware VM candidates build managed host payloads', async () => {
|
||||
const restoreFetch = installStandaloneEsxiSoapStub();
|
||||
const connection = {
|
||||
id: 'vc_host',
|
||||
name: 'ESXi 01',
|
||||
target_type: 'host',
|
||||
base_url: 'https://esxi01.lab.local',
|
||||
username: 'root',
|
||||
password: 'secret',
|
||||
enabled: 1
|
||||
};
|
||||
const preview = await previewVCenterHosts({ connection, filter: { field: 'hostname', operator: 'contains', value: 'eng-ent' } });
|
||||
const payload = buildHostPayloadFromVm(preview.candidates[0], {
|
||||
connectionId: 'vc_host',
|
||||
credentialId: 'cred_1',
|
||||
transport: 'winrm',
|
||||
tags: ['lab'],
|
||||
visibility: 'shared'
|
||||
});
|
||||
const power = await getVCenterVmPowerState(connection, preview.candidates[0].id);
|
||||
restoreFetch();
|
||||
|
||||
assert.equal(payload.name, 'ENG-ENT-APP01');
|
||||
assert.equal(payload.address, 'eng-ent-app01.contoso.local');
|
||||
assert.equal(payload.transport, 'winrm');
|
||||
assert.equal(payload.sourceType, 'vmware');
|
||||
assert.equal(payload.sourceConnectionId, 'vc_host');
|
||||
assert.equal(payload.sourceRef, 'vim.VirtualMachine:101');
|
||||
assert.ok(payload.tags.includes('standalone-esxi'));
|
||||
assert.ok(payload.tags.includes('esxi-vm:vim.VirtualMachine:101'));
|
||||
assert.equal(power.checked, true);
|
||||
assert.equal(power.state, 'POWERED_ON');
|
||||
});
|
||||
|
||||
test('standalone VMware host candidates build managed host payloads', () => {
|
||||
const candidate = buildStandaloneHostCandidate(
|
||||
{ id: 'vc_host', name: 'ESXi 01' },
|
||||
{ baseUrl: 'https://esxi01.lab.local', targetType: 'host' }
|
||||
);
|
||||
const payload = buildHostPayloadFromVm(candidate, {
|
||||
connectionId: 'vc_host',
|
||||
credentialId: 'cred_1',
|
||||
transport: 'ssh',
|
||||
tags: ['lab'],
|
||||
visibility: 'shared'
|
||||
});
|
||||
|
||||
assert.equal(payload.name, 'ESXi 01');
|
||||
assert.equal(payload.address, 'esxi01.lab.local');
|
||||
assert.equal(payload.transport, 'ssh');
|
||||
assert.equal(payload.credentialId, 'cred_1');
|
||||
assert.equal(payload.sourceType, 'vmware');
|
||||
assert.equal(payload.sourceConnectionId, 'vc_host');
|
||||
assert.equal(payload.sourceRef, 'standalone-esxi-host');
|
||||
assert.ok(payload.tags.includes('standalone-esxi'));
|
||||
assert.ok(payload.tags.includes('lab'));
|
||||
});
|
||||
|
||||
test('normalizeVCenterVm maps guest identity and networking into a host candidate', () => {
|
||||
|
||||
Reference in New Issue
Block a user