254 lines
11 KiB
JavaScript
254 lines
11 KiB
JavaScript
import './setup.mjs';
|
|
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { load } from './setup.mjs';
|
|
|
|
const {
|
|
buildVmListPath,
|
|
buildHostPayloadFromVm,
|
|
buildStandaloneHostCandidate,
|
|
filterSummaryVms,
|
|
getVCenterVmPowerState,
|
|
normalizeBaseUrl,
|
|
normalizeVCenterVm,
|
|
previewVCenterHosts,
|
|
settingsFromVCenterConnection,
|
|
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 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');
|
|
|
|
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', () => {
|
|
const candidate = normalizeVCenterVm(
|
|
{ vm: 'vm-42', name: 'ENG-ENT-APP01', power_state: 'POWERED_ON' },
|
|
{ host_name: 'eng-ent-app01.contoso.local', ip_address: '10.42.1.20', full_name: { default_message: 'Microsoft Windows Server 2022' } },
|
|
[{ ip: { ip_addresses: [{ ip_address: 'fe80::1' }, { ip_address: '10.42.1.21' }] } }]
|
|
);
|
|
|
|
assert.equal(candidate.id, 'vm-42');
|
|
assert.equal(candidate.name, 'ENG-ENT-APP01');
|
|
assert.equal(candidate.fqdn, 'eng-ent-app01.contoso.local');
|
|
assert.equal(candidate.address, 'eng-ent-app01.contoso.local');
|
|
assert.deepEqual(candidate.ipAddresses, ['10.42.1.20', '10.42.1.21']);
|
|
assert.equal(candidate.osFamily, 'windows');
|
|
assert.equal(candidate.guestFullName, 'Microsoft Windows Server 2022');
|
|
});
|
|
|
|
test('normalizeVCenterVm marks unknown guest operating systems as other', () => {
|
|
const candidate = normalizeVCenterVm(
|
|
{ vm: 'vm-unknown', name: 'MYSTERY-01', power_state: 'POWERED_ON' },
|
|
null,
|
|
[]
|
|
);
|
|
|
|
assert.equal(candidate.osFamily, 'other');
|
|
});
|
|
|
|
test('vmMatchesFilter supports hostname contains and IP equals matching', () => {
|
|
const candidate = { name: 'ENG-ENT-APP01', fqdn: 'app01.contoso.local', address: 'app01.contoso.local', ipAddress: '10.42.1.20', ipAddresses: ['10.42.1.20'] };
|
|
|
|
assert.equal(vmMatchesFilter(candidate, { field: 'hostname', operator: 'contains', value: 'eng-ent' }), true);
|
|
assert.equal(vmMatchesFilter(candidate, { field: 'hostname', operator: 'contains', value: 'contoso' }), true);
|
|
assert.equal(vmMatchesFilter(candidate, { field: 'ip', operator: 'equals', value: '10.42.1.20' }), true);
|
|
assert.equal(vmMatchesFilter(candidate, { field: 'fqdn', operator: 'startsWith', value: 'db' }), false);
|
|
});
|
|
|
|
test('vmMatchesFilter matches guest-only host names after enrichment', () => {
|
|
const candidate = normalizeVCenterVm(
|
|
{ vm: 'vm-200', name: 'KSUS01PAPP01', power_state: 'POWERED_ON' },
|
|
{ host_name: 'MCS-WIN-APP01.ent.mitekindustries.com', ip_address: '10.42.5.44', family: 'WINDOWS' },
|
|
[]
|
|
);
|
|
|
|
assert.equal(vmMatchesFilter(candidate, { field: 'hostname', operator: 'contains', value: 'MCS' }), true);
|
|
assert.equal(vmMatchesFilter(candidate, { field: 'name', operator: 'contains', value: 'MCS' }), false);
|
|
});
|
|
|
|
test('filterSummaryVms scans the full VM summary list for VM-name filters', () => {
|
|
const vms = [
|
|
...Array.from({ length: 900 }, (_, index) => ({ vm: `vm-${index}`, name: `ZZZ-${index}` })),
|
|
{ vm: 'vm-901', name: 'MCS-WIN-APP01' }
|
|
];
|
|
const result = filterSummaryVms(vms, { field: 'name', operator: 'contains', value: 'MCS' });
|
|
|
|
assert.equal(result.summaryFilterApplied, true);
|
|
assert.equal(result.summaryMatched, 1);
|
|
assert.equal(result.vms.length, 1);
|
|
assert.equal(result.vms[0].name, 'MCS-WIN-APP01');
|
|
});
|
|
|
|
test('filterSummaryVms prefilters hostname searches by VM name and falls back when needed', () => {
|
|
const vms = [{ vm: 'vm-1', name: 'MCS-WIN-APP01' }, { vm: 'vm-2', name: 'SQL-01' }];
|
|
const matched = filterSummaryVms(vms, { field: 'hostname', operator: 'contains', value: 'MCS' });
|
|
const fallback = filterSummaryVms(vms, { field: 'hostname', operator: 'contains', value: 'guest-only-name' });
|
|
|
|
assert.equal(matched.summaryMatched, 1);
|
|
assert.equal(matched.summaryFilterFallback, false);
|
|
assert.deepEqual(matched.vms.map((vm) => vm.vm), ['vm-1', 'vm-2']);
|
|
assert.equal(fallback.summaryMatched, 0);
|
|
assert.equal(fallback.summaryFilterFallback, true);
|
|
assert.equal(fallback.vms.length, 2);
|
|
});
|
|
|
|
test('buildVmListPath uses documented exact-name filters for api and rest profiles', () => {
|
|
const filter = { field: 'name', operator: 'equals', value: 'MCS-WIN-APP01' };
|
|
|
|
assert.equal(
|
|
buildVmListPath({ vmListPath: '/api/vcenter/vm', vmNameParam: 'names' }, filter),
|
|
'/api/vcenter/vm?names=MCS-WIN-APP01'
|
|
);
|
|
assert.equal(
|
|
buildVmListPath({ vmListPath: '/rest/vcenter/vm', vmNameParam: 'filter.names' }, filter),
|
|
'/rest/vcenter/vm?filter.names=MCS-WIN-APP01'
|
|
);
|
|
assert.equal(
|
|
buildVmListPath({ vmListPath: '/api/vcenter/vm', vmNameParam: 'names' }, { field: 'name', operator: 'contains', value: 'MCS' }),
|
|
'/api/vcenter/vm'
|
|
);
|
|
});
|
|
|
|
test('buildHostPayloadFromVm attaches credential, tags, transport, and import notes', () => {
|
|
const payload = buildHostPayloadFromVm(
|
|
{
|
|
id: 'vm-42',
|
|
name: 'ENG-ENT-APP01',
|
|
fqdn: 'eng-ent-app01.contoso.local',
|
|
address: 'eng-ent-app01.contoso.local',
|
|
osFamily: 'windows',
|
|
powerState: 'POWERED_ON',
|
|
guestFullName: 'Microsoft Windows Server 2022',
|
|
ipAddresses: ['10.42.1.20']
|
|
},
|
|
{ credentialId: 'cred_1', connectionId: 'vc_1', transport: 'winrm', tags: ['engineering'], visibility: 'group', groupId: 'grp_1' }
|
|
);
|
|
|
|
assert.equal(payload.credentialId, 'cred_1');
|
|
assert.equal(payload.transport, 'winrm');
|
|
assert.equal(payload.visibility, 'group');
|
|
assert.equal(payload.groupId, 'grp_1');
|
|
assert.equal(payload.sourceType, 'vmware');
|
|
assert.equal(payload.sourceConnectionId, 'vc_1');
|
|
assert.equal(payload.sourceRef, 'vm-42');
|
|
assert.ok(payload.tags.includes('vcenter:vm-42'));
|
|
assert.ok(payload.tags.includes('engineering'));
|
|
assert.match(payload.notes, /Imported from VMware vCenter VM vm-42/);
|
|
});
|