Files
poshmanager/server/test/packageDoc.test.mjs
2026-08-09 17:33:11 -05:00

96 lines
4.0 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { buildDatasheetModel, renderDatasheet } from '../services/packageDocService.js';
function sampleDeployment(overrides = {}) {
return {
id: 'intune_1',
name: 'Acme Reader 24.1',
profileId: 'psadt_1',
profileName: 'Acme Reader',
applicationId: null,
appType: 'Windows app (Win32)',
commandStyle: 'v4',
installBehavior: 'system',
restartBehavior: 'return-code',
sourceFolder: 'C:\\packages\\reader',
intunewinFile: 'reader.intunewin',
installCommand: 'Invoke-AppDeployToolkit.exe -DeploymentType Install',
uninstallCommand: 'Invoke-AppDeployToolkit.exe -DeploymentType Uninstall',
detectionType: 'msi-product-code',
detectionRule: '{12345678-ABCD-1234-ABCD-1234567890AB}',
requirements: { architecture: 'x64', minOs: 'Windows 10 22H2', diskSpaceMb: 250, runAs32Bit: false },
returnCodes: [
{ code: 0, type: 'success', meaning: 'Install completed.' },
{ code: 3010, type: 'softReboot', meaning: 'Reboot required.' }
],
assignments: [
{ ring: 'Pilot', intent: 'available', groupName: 'IT-Pilot', notes: 'Validation ring.' }
],
status: 'draft',
notes: 'Created from the recipe.',
...overrides
};
}
const sampleProfile = {
name: 'Acme Reader',
appVendor: 'Acme',
appName: 'Reader',
appVersion: '24.1',
appArch: 'x64'
};
test('buildDatasheetModel pulls metadata from the profile and lays out every section', () => {
const model = buildDatasheetModel({ deployment: sampleDeployment(), profile: sampleProfile });
assert.equal(model.title, 'Acme Reader 24.1');
assert.equal(model.subtitle, 'Acme Reader 24.1');
const headings = model.sections.map((s) => s.heading);
assert.deepEqual(headings, ['Overview', 'Install & uninstall', 'Detection', 'Return codes', 'Assignments', 'Requirements', 'Source & version', 'Notes']);
});
test('buildDatasheetModel requires a deployment', () => {
assert.throws(() => buildDatasheetModel({}), /deployment is required/);
});
test('buildDatasheetModel omits the Notes section when there are no notes', () => {
const model = buildDatasheetModel({ deployment: sampleDeployment({ notes: '' }), profile: sampleProfile });
assert.ok(!model.sections.some((s) => s.heading === 'Notes'));
});
test('renderDatasheet markdown carries commands, detection, return codes and assignments', () => {
const doc = renderDatasheet({ deployment: sampleDeployment(), profile: sampleProfile }, { format: 'md' });
assert.equal(doc.format, 'md');
assert.equal(doc.extension, 'md');
assert.match(doc.contentType, /text\/markdown/);
assert.match(doc.content, /^# Acme Reader 24\.1/);
assert.match(doc.content, /Invoke-AppDeployToolkit\.exe -DeploymentType Install/);
assert.match(doc.content, /\{12345678-ABCD-1234-ABCD-1234567890AB\}/);
assert.match(doc.content, /\| 3010 \| Soft reboot \| Reboot required\. \|/);
assert.match(doc.content, /\| Pilot \| Available \| IT-Pilot \| Validation ring\. \|/);
});
test('renderDatasheet html is self-contained and escapes user content', () => {
const doc = renderDatasheet(
{ deployment: sampleDeployment({ notes: 'Watch <script> & "quotes"' }), profile: sampleProfile },
{ format: 'html' }
);
assert.equal(doc.format, 'html');
assert.equal(doc.extension, 'html');
assert.match(doc.contentType, /text\/html/);
assert.match(doc.content, /^<!doctype html>/);
assert.match(doc.content, /Watch &lt;script&gt; &amp; &quot;quotes&quot;/);
assert.ok(!doc.content.includes('<script>'), 'no unescaped script tag leaks through');
});
test('renderDatasheet shows empty tables gracefully and defaults to markdown', () => {
const doc = renderDatasheet({ deployment: sampleDeployment({ assignments: [], returnCodes: [] }) });
assert.equal(doc.format, 'md');
assert.match(doc.content, /_None_/);
});
test('renderDatasheet falls back to the deployment name without a profile', () => {
const doc = renderDatasheet({ deployment: sampleDeployment({ profileId: null }) }, { format: 'md' });
assert.match(doc.content, /# Acme Reader 24\.1/);
});