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'); });