This commit is contained in:
2026-06-25 12:05:53 -05:00
commit b58e50e423
141 changed files with 28190 additions and 0 deletions

162
server/test/intune.test.mjs Normal file
View 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');
});