Initial
This commit is contained in:
162
server/test/intune.test.mjs
Normal file
162
server/test/intune.test.mjs
Normal file
@@ -0,0 +1,162 @@
|
||||
import { adminUserId, load } from './setup.mjs';
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
const {
|
||||
buildWin32LobAppPayload,
|
||||
buildAssignmentsPayload,
|
||||
buildRelationshipsPayload,
|
||||
validateRelationships,
|
||||
MAX_SUPERSEDENCE_TARGETS
|
||||
} = await load('../services/intunePublishService.js');
|
||||
const { createGraphConnection, recordGraphAudit, listGraphAudit } = await load('../models/graphModel.js');
|
||||
|
||||
const owner = adminUserId();
|
||||
|
||||
function deploymentFixture(overrides = {}) {
|
||||
return {
|
||||
id: 'intune_fixture',
|
||||
name: 'Contoso VPN',
|
||||
notes: 'Pilot ring',
|
||||
intunewinFile: 'ContosoVpn.intunewin',
|
||||
installBehavior: 'system',
|
||||
restartBehavior: 'return-code',
|
||||
installCommand: 'Invoke-AppDeployToolkit.exe -DeploymentType Install',
|
||||
uninstallCommand: 'Invoke-AppDeployToolkit.exe -DeploymentType Uninstall',
|
||||
detectionType: 'msi-product-code',
|
||||
detectionRule: '{12345678-1234-1234-1234-123456789012}',
|
||||
returnCodes: [{ code: 3010, type: 'softReboot' }, { code: 1602, type: 'retry' }],
|
||||
requirements: { architecture: 'both', minOs: 'Windows 10 22H2', diskSpaceMb: 512, runAs32Bit: false },
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
|
||||
const profileFixture = { appVendor: 'Contoso', appName: 'VPN Client', appVersion: '3.1.0' };
|
||||
|
||||
test('builds a win32LobApp payload with correct core shape', () => {
|
||||
const { payload } = buildWin32LobAppPayload(deploymentFixture(), profileFixture);
|
||||
assert.equal(payload['@odata.type'], '#microsoft.graph.win32LobApp');
|
||||
assert.equal(payload.displayName, 'Contoso VPN Client 3.1.0');
|
||||
assert.equal(payload.publisher, 'Contoso');
|
||||
assert.equal(payload.installExperience.runAsAccount, 'system');
|
||||
assert.equal(payload.installExperience.deviceRestartBehavior, 'basedOnReturnCode');
|
||||
assert.equal(payload.applicableArchitectures, 'x64,x86');
|
||||
assert.equal(payload.minimumSupportedOperatingSystem.v10_22H2, true);
|
||||
});
|
||||
|
||||
test('return codes always include 0=success and clamp unknown types', () => {
|
||||
const { payload } = buildWin32LobAppPayload(deploymentFixture({ returnCodes: [{ code: 7, type: 'bogus' }] }));
|
||||
assert.ok(payload.returnCodes.some((rc) => rc.returnCode === 0 && rc.type === 'success'));
|
||||
assert.equal(payload.returnCodes.find((rc) => rc.returnCode === 7).type, 'failed');
|
||||
});
|
||||
|
||||
test('MSI detection maps to product code rule', () => {
|
||||
const { payload } = buildWin32LobAppPayload(deploymentFixture());
|
||||
const rule = payload.detectionRules[0];
|
||||
assert.equal(rule['@odata.type'], '#microsoft.graph.win32LobAppProductCodeDetection');
|
||||
assert.equal(rule.productCode, '{12345678-1234-1234-1234-123456789012}');
|
||||
});
|
||||
|
||||
test('custom-script detection base64-encodes the script body', () => {
|
||||
const { payload } = buildWin32LobAppPayload(deploymentFixture({ detectionType: 'custom-script', detectionRule: 'Test-Path C:\\app.exe' }));
|
||||
const rule = payload.detectionRules[0];
|
||||
assert.equal(rule['@odata.type'], '#microsoft.graph.win32LobAppPowerShellScriptDetection');
|
||||
assert.equal(Buffer.from(rule.scriptContent, 'base64').toString('utf8'), 'Test-Path C:\\app.exe');
|
||||
});
|
||||
|
||||
test('missing .intunewin produces a content warning', () => {
|
||||
const { warnings } = buildWin32LobAppPayload(deploymentFixture({ intunewinFile: '' }));
|
||||
assert.ok(warnings.some((w) => w.toLowerCase().includes('content must be uploaded')));
|
||||
});
|
||||
|
||||
// --- Phase 3: assignments -------------------------------------------------
|
||||
|
||||
test('group assignment maps to groupAssignmentTarget with intent', () => {
|
||||
const { assignments } = buildAssignmentsPayload({
|
||||
assignments: [{ ring: 'Broad', intent: 'required', groupMode: 'group', groupId: 'g-1' }]
|
||||
});
|
||||
assert.equal(assignments.length, 1);
|
||||
assert.equal(assignments[0].intent, 'required');
|
||||
assert.equal(assignments[0].target['@odata.type'], '#microsoft.graph.groupAssignmentTarget');
|
||||
assert.equal(assignments[0].target.groupId, 'g-1');
|
||||
});
|
||||
|
||||
test('exclude intent maps to exclusionGroupAssignmentTarget', () => {
|
||||
const { assignments } = buildAssignmentsPayload({
|
||||
assignments: [{ ring: 'Exclusions', intent: 'exclude', groupMode: 'group', groupId: 'g-x' }]
|
||||
});
|
||||
assert.equal(assignments[0].target['@odata.type'], '#microsoft.graph.exclusionGroupAssignmentTarget');
|
||||
assert.ok(['required', 'available', 'uninstall'].includes(assignments[0].intent));
|
||||
});
|
||||
|
||||
test('all-devices target and assignment filters are honored', () => {
|
||||
const { assignments } = buildAssignmentsPayload({
|
||||
assignments: [{ ring: 'AllDev', intent: 'available', groupMode: 'allDevices', filterId: 'f-1', filterType: 'exclude', autoUpdate: true }]
|
||||
});
|
||||
assert.equal(assignments[0].target['@odata.type'], '#microsoft.graph.allDevicesAssignmentTarget');
|
||||
assert.equal(assignments[0].target.deviceAndAppManagementAssignmentFilterId, 'f-1');
|
||||
assert.equal(assignments[0].target.deviceAndAppManagementAssignmentFilterType, 'exclude');
|
||||
assert.equal(assignments[0].settings.autoUpdateSettings.autoUpdateSupersededApps, 'enabled');
|
||||
});
|
||||
|
||||
test('group assignment without an object id is skipped with a warning', () => {
|
||||
const { assignments, warnings } = buildAssignmentsPayload({
|
||||
assignments: [{ ring: 'Broad', intent: 'required', groupMode: 'group', groupId: '' }]
|
||||
});
|
||||
assert.equal(assignments.length, 0);
|
||||
assert.ok(warnings.some((w) => w.includes('Broad')));
|
||||
});
|
||||
|
||||
// --- Phase 4: relationships ----------------------------------------------
|
||||
|
||||
test('supersedence and dependency relationships map to correct Graph types', () => {
|
||||
const { relationships } = buildRelationshipsPayload([
|
||||
{ kind: 'supersedence', targetAppId: 'old-app', mode: 'replace' },
|
||||
{ kind: 'dependency', targetAppId: 'dep-app', mode: 'autoInstall' }
|
||||
], 'self-app');
|
||||
assert.equal(relationships[0]['@odata.type'], '#microsoft.graph.mobileAppSupersedence');
|
||||
assert.equal(relationships[0].supersedenceType, 'replace');
|
||||
assert.equal(relationships[1]['@odata.type'], '#microsoft.graph.mobileAppDependency');
|
||||
assert.equal(relationships[1].dependencyType, 'autoInstall');
|
||||
});
|
||||
|
||||
test('relationship validation rejects self-reference and duplicates', () => {
|
||||
const errors = validateRelationships([
|
||||
{ kind: 'supersedence', targetAppId: 'self' },
|
||||
{ kind: 'supersedence', targetAppId: 'a' },
|
||||
{ kind: 'supersedence', targetAppId: 'a' }
|
||||
], 'self');
|
||||
assert.ok(errors.some((e) => e.includes('itself')));
|
||||
assert.ok(errors.some((e) => e.toLowerCase().includes('duplicate')));
|
||||
});
|
||||
|
||||
test('relationship validation enforces the supersedence node ceiling', () => {
|
||||
const many = Array.from({ length: MAX_SUPERSEDENCE_TARGETS + 1 }, (_, i) => ({ kind: 'supersedence', targetAppId: `app-${i}` }));
|
||||
const errors = validateRelationships(many, 'self');
|
||||
assert.ok(errors.some((e) => e.includes('Too many supersedence')));
|
||||
assert.throws(() => buildRelationshipsPayload(many, 'self'));
|
||||
});
|
||||
|
||||
test('graph connection persists the allow_write write-gate', () => {
|
||||
const conn = createGraphConnection({
|
||||
name: 'WriteConn', tenantId: 'contoso', clientId: 'abc', clientSecret: 'shh',
|
||||
allowWrite: true, visibility: 'personal'
|
||||
}, owner);
|
||||
assert.equal(conn.allowWrite, true);
|
||||
});
|
||||
|
||||
test('audit log records and reads back request/response payloads', () => {
|
||||
// connection/deployment are nullable FKs; use null here and assert the
|
||||
// payload round-trips. (Real publishes pass real ids.)
|
||||
recordGraphAudit({
|
||||
connectionId: null, deploymentId: null, actorUserId: owner, actorEmail: 'a@b.c',
|
||||
action: 'win32app.create', targetGraphId: 'app123', status: 'success', message: 'ok',
|
||||
request: { displayName: 'X' }, response: { id: 'app123' }
|
||||
});
|
||||
const rows = listGraphAudit({ limit: 5 });
|
||||
const entry = rows.find((r) => r.targetGraphId === 'app123');
|
||||
assert.ok(entry, 'audit entry should be retrievable');
|
||||
assert.equal(entry.action, 'win32app.create');
|
||||
assert.equal(entry.request.displayName, 'X');
|
||||
assert.equal(entry.response.id, 'app123');
|
||||
});
|
||||
Reference in New Issue
Block a user