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

View File

@@ -0,0 +1,78 @@
import { adminUserId, makeUser, load } from './setup.mjs';
import test from 'node:test';
import assert from 'node:assert/strict';
const { compareVersions, evaluateUpdateState, mapGraphAppToCatalog } = await load('../services/appVersionService.js');
const { createApplication, listApplications, loadApplication, updateApplication, deleteApplication, recordVersionCheck } = await load('../models/applicationModel.js');
const { runCatalogChecks } = await load('../services/catalogWorker.js');
const owner = adminUserId();
const other = makeUser({ email: 'app-other@test.local' });
// --- pure version logic ---------------------------------------------------
test('compareVersions orders numeric segments correctly', () => {
assert.equal(compareVersions('1.2.0', '1.10.0'), -1);
assert.equal(compareVersions('2.0', '1.9.9'), 1);
assert.equal(compareVersions('1.0.0', '1.0.0'), 0);
assert.equal(compareVersions('v3.1', '3.1.0'), 0);
});
test('evaluateUpdateState flags newer upstream versions', () => {
assert.equal(evaluateUpdateState({ currentVersion: '1.0.0', latestKnownVersion: '1.1.0' }).updateAvailable, true);
assert.equal(evaluateUpdateState({ currentVersion: '2.0.0', latestKnownVersion: '1.1.0' }).updateAvailable, false);
assert.equal(evaluateUpdateState({ currentVersion: '', latestKnownVersion: '1.0.0' }).updateAvailable, true);
assert.equal(evaluateUpdateState({ currentVersion: '1.0.0', latestKnownVersion: '' }).updateAvailable, false);
});
test('mapGraphAppToCatalog maps the key fields', () => {
const mapped = mapGraphAppToCatalog({ id: 'app-1', displayName: 'Contoso VPN', publisher: 'Contoso', displayVersion: '3.1.0' });
assert.equal(mapped.name, 'Contoso VPN');
assert.equal(mapped.vendor, 'Contoso');
assert.equal(mapped.currentVersion, '3.1.0');
assert.equal(mapped.currentGraphAppId, 'app-1');
});
// --- catalog model --------------------------------------------------------
test('applications are visibility-scoped', () => {
const app = createApplication({ name: 'Private App', currentVersion: '1.0.0', visibility: 'personal' }, owner);
assert.ok(listApplications(owner).some((a) => a.id === app.id));
assert.ok(!listApplications(other).some((a) => a.id === app.id));
assert.equal(loadApplication(app.id, other), null);
});
test('application read surfaces a computed updateState', () => {
const app = createApplication({ name: 'Edge', currentVersion: '1.0.0', latestKnownVersion: '1.2.0', visibility: 'shared' }, owner);
const loaded = loadApplication(app.id, owner);
assert.equal(loaded.updateState.updateAvailable, true);
assert.equal(loaded.updateState.latest, '1.2.0');
});
test('recordVersionCheck updates the latest known version', () => {
const app = createApplication({ name: 'Firefox', currentVersion: '120.0', visibility: 'shared' }, owner);
const updated = recordVersionCheck(app.id, owner, { latestKnownVersion: '121.0', message: '' });
assert.equal(updated.latestKnownVersion, '121.0');
assert.equal(updated.updateState.updateAvailable, true);
assert.ok(updated.lastCheckedAt);
});
test('auto-check worker only processes opted-in apps and records new versions', async () => {
const optedIn = createApplication({ name: 'Worker Opt-In', currentVersion: '1.0.0', latestKnownVersion: '1.0.0', versionSource: 'url', versionSourceRef: 'https://example/v', autoCheck: true, visibility: 'shared' }, owner);
const optedOut = createApplication({ name: 'Worker Opt-Out', currentVersion: '1.0.0', latestKnownVersion: '1.0.0', autoCheck: false, visibility: 'shared' }, owner);
// Injected fetcher avoids network; returns a newer version.
const summary = await runCatalogChecks({ fetcher: async () => ({ version: '2.0.0', message: '' }) });
assert.ok(summary.checked >= 1);
assert.ok(summary.updated >= 1);
assert.equal(loadApplication(optedIn.id, owner).latestKnownVersion, '2.0.0');
assert.equal(loadApplication(optedOut.id, owner).latestKnownVersion, '1.0.0');
});
test('a non-owner cannot update or delete a personal application', () => {
const app = createApplication({ name: 'Locked App', visibility: 'personal' }, owner);
assert.equal(updateApplication(app.id, { name: 'Hijacked' }, other), null);
deleteApplication(app.id, other);
assert.ok(loadApplication(app.id, owner), 'should survive foreign delete');
});

View File

@@ -0,0 +1,54 @@
import { db, adminUserId, load } from './setup.mjs';
import test from 'node:test';
import assert from 'node:assert/strict';
const { evaluatePromotion, runAutoPromotions } = await load('../services/promotionWorker.js');
const { createIntuneDeployment, loadDeploymentRaw } = await load('../models/psadtModel.js');
const owner = adminUserId();
const hoursAgo = (h) => new Date(Date.now() - h * 3_600_000).toISOString();
// --- pure eligibility -----------------------------------------------------
test('evaluatePromotion is due once the soak window elapses', () => {
const d = { autoPromoteAfterHours: 1, autoPromoteRing: 'Broad', soakStartedAt: hoursAgo(2), promotedAt: '' };
assert.equal(evaluatePromotion(d).due, true);
});
test('evaluatePromotion is not due while soaking, disabled, or already promoted', () => {
assert.equal(evaluatePromotion({ autoPromoteAfterHours: 4, autoPromoteRing: 'Broad', soakStartedAt: hoursAgo(1), promotedAt: '' }).due, false);
assert.equal(evaluatePromotion({ autoPromoteAfterHours: 0, autoPromoteRing: 'Broad', soakStartedAt: hoursAgo(5) }).due, false);
assert.equal(evaluatePromotion({ autoPromoteAfterHours: 1, autoPromoteRing: 'Broad', soakStartedAt: hoursAgo(5), promotedAt: hoursAgo(1) }).due, false);
assert.equal(evaluatePromotion({ autoPromoteAfterHours: 1, autoPromoteRing: '', soakStartedAt: hoursAgo(5) }).due, false);
});
// --- worker run -----------------------------------------------------------
test('runAutoPromotions promotes an eligible deployment and skips a soaking one', async () => {
const eligible = createIntuneDeployment({
name: 'Promote Me', visibility: 'shared', autoPromoteAfterHours: 1, autoPromoteRing: 'Broad', autoPromoteIntent: 'required',
assignments: [{ ring: 'Broad', intent: 'available', groupId: 'g1', groupMode: 'group' }]
}, owner);
const soaking = createIntuneDeployment({
name: 'Still Soaking', visibility: 'shared', autoPromoteAfterHours: 8, autoPromoteRing: 'Broad', autoPromoteIntent: 'required',
assignments: [{ ring: 'Broad', intent: 'available', groupId: 'g2', groupMode: 'group' }]
}, owner);
// Start the soak clock: eligible elapsed, soaking just started.
db.prepare('UPDATE intune_deployments SET soak_started_at = ? WHERE id = ?').run(hoursAgo(3), eligible.id);
db.prepare('UPDATE intune_deployments SET soak_started_at = ? WHERE id = ?').run(hoursAgo(1), soaking.id);
let pushed = 0;
const summary = await runAutoPromotions({ pusher: async () => { pushed += 1; } });
assert.ok(summary.promoted >= 1);
const promoted = loadDeploymentRaw(eligible.id);
assert.equal(promoted.assignments.find((a) => a.ring === 'Broad').intent, 'required');
assert.ok(promoted.promotedAt);
const stillSoaking = loadDeploymentRaw(soaking.id);
assert.equal(stillSoaking.assignments.find((a) => a.ring === 'Broad').intent, 'available');
assert.equal(stillSoaking.promotedAt, '');
// Neither deployment has a Graph connection, so nothing was pushed.
assert.equal(pushed, 0);
});

View File

@@ -0,0 +1,38 @@
import './setup.mjs';
import test from 'node:test';
import assert from 'node:assert/strict';
import { load } from './setup.mjs';
const { buildPackagerInvocation, expectedOutputFile, resolveToolPath, builderAvailability } = await load('../services/intuneWinBuilder.js');
test('bundled invocation uses IntuneWinAppUtil flags', () => {
const inv = buildPackagerInvocation({ toolPath: 'C:/tools/IntuneWinAppUtil.exe', sourceFolder: 'C:/src', setupFile: 'setup.exe', outputDir: 'C:/out' });
assert.equal(inv.shell, false);
assert.equal(inv.command, 'C:/tools/IntuneWinAppUtil.exe');
assert.deepEqual(inv.args, ['-c', 'C:/src', '-s', 'setup.exe', '-o', 'C:/out', '-q']);
});
test('external command template substitutes placeholders', () => {
const inv = buildPackagerInvocation({
commandTemplate: 'pack --src {source} --setup {setup} --out {output}',
sourceFolder: '/s', setupFile: 'a.msi', outputDir: '/o'
});
assert.equal(inv.shell, true);
assert.equal(inv.command, 'pack --src /s --setup a.msi --out /o');
});
test('expectedOutputFile names after the setup base name', () => {
assert.ok(expectedOutputFile('/out', 'ContosoVpnSetup.exe').endsWith('ContosoVpnSetup.intunewin'));
assert.ok(expectedOutputFile('/out', 'app.msi').endsWith('app.intunewin'));
});
test('resolveToolPath defaults into the tools directory', () => {
assert.ok(resolveToolPath().toLowerCase().includes('intunewinapputil.exe'));
});
test('builderAvailability reports unavailable when the tool is missing on this host', () => {
// The test host has no tool placed and is not Windows, so build is unavailable.
const availability = builderAvailability();
assert.equal(availability.available, false);
assert.ok(availability.reason);
});

View File

@@ -0,0 +1,62 @@
import './setup.mjs';
import test from 'node:test';
import assert from 'node:assert/strict';
import { load } from './setup.mjs';
const { computeDrift } = await load('../services/intuneDriftService.js');
const { canPublish, canApprove } = await load('../services/graphRbac.js');
// --- drift ----------------------------------------------------------------
test('computeDrift reports in-sync when plan matches tenant', () => {
const app = { displayName: 'Edge', publisher: 'Microsoft', installCommandLine: 'a', uninstallCommandLine: 'b', applicableArchitectures: 'x64' };
const assignment = { intent: 'required', target: { '@odata.type': '#microsoft.graph.groupAssignmentTarget', groupId: 'g1' } };
const rel = { '@odata.type': '#microsoft.graph.mobileAppSupersedence', targetId: 'old' };
const drift = computeDrift({
expectedApp: app, liveApp: { ...app },
expectedAssignments: [assignment], liveAssignments: [assignment],
expectedRelationships: [rel], liveRelationships: [rel]
});
assert.equal(drift.inSync, true);
});
test('computeDrift detects field, assignment, and relationship differences', () => {
const drift = computeDrift({
expectedApp: { displayName: 'Edge', installCommandLine: 'new' },
liveApp: { displayName: 'Edge', installCommandLine: 'old' },
expectedAssignments: [{ intent: 'required', target: { '@odata.type': '#microsoft.graph.groupAssignmentTarget', groupId: 'g1' } }],
liveAssignments: [{ intent: 'available', target: { '@odata.type': '#microsoft.graph.groupAssignmentTarget', groupId: 'g1' } }],
expectedRelationships: [{ '@odata.type': '#microsoft.graph.mobileAppSupersedence', targetId: 'old' }],
liveRelationships: []
});
assert.equal(drift.inSync, false);
assert.ok(drift.appDrift.some((d) => d.field === 'installCommandLine'));
assert.equal(drift.assignmentDrift.missing.length, 1); // required|...|g1 missing in tenant
assert.equal(drift.assignmentDrift.extra.length, 1); // available|...|g1 extra in tenant
assert.equal(drift.relationshipDrift.missing.length, 1);
});
// --- per-connection RBAC --------------------------------------------------
const admin = { id: 'u-admin', role: 'admin' };
const alice = { id: 'u-alice', role: 'user' };
const bob = { id: 'u-bob', role: 'user' };
test('canPublish: admins always; empty list allows all; otherwise listed only', () => {
assert.equal(canPublish({ publisherIds: [] }, admin), true);
assert.equal(canPublish({ publisherIds: [] }, alice), true);
assert.equal(canPublish({ publisherIds: ['u-alice'] }, alice), true);
assert.equal(canPublish({ publisherIds: ['u-alice'] }, bob), false);
});
test('canApprove: admins always; otherwise only named approvers (empty = none)', () => {
assert.equal(canApprove({ approverIds: [] }, admin), true);
assert.equal(canApprove({ approverIds: [] }, alice), false);
assert.equal(canApprove({ approverIds: ['u-alice'] }, alice), true);
assert.equal(canApprove({ approverIds: ['u-alice'] }, bob), false);
});
test('RBAC helpers tolerate JSON-string list columns', () => {
assert.equal(canPublish({ publisher_ids: '["u-alice"]' }, alice), true);
assert.equal(canApprove({ approver_ids: '["u-bob"]' }, bob), true);
});

View File

@@ -0,0 +1,19 @@
import './setup.mjs';
import test from 'node:test';
import assert from 'node:assert/strict';
import { load } from './setup.mjs';
const { isEntraLoginConfigured, signState, verifyState } = await load('../services/entraService.js');
test('Entra login is disabled when not configured', () => {
// The test environment leaves ENTRA_* unset, so login must not be offered.
assert.equal(isEntraLoginConfigured(), false);
});
test('signed state round-trips and rejects tampering', () => {
const state = signState({ nonce: 42 });
const decoded = verifyState(state);
assert.equal(decoded.purpose, 'entra-state');
assert.throws(() => verifyState(state + 'tampered'));
assert.throws(() => verifyState('not-a-token'));
});

View File

@@ -0,0 +1,71 @@
import { db, adminUserId, makeUser, load } from './setup.mjs';
import test from 'node:test';
import assert from 'node:assert/strict';
const {
createChangeRequest, findApprovedRequest, findPendingRequest, decideChangeRequest, markRequestExecuted, listChangeRequests
} = await load('../models/changeRequestModel.js');
const { intuneReportingOverview, recordGraphAudit } = await load('../models/graphModel.js');
const { createIntuneDeployment } = await load('../models/psadtModel.js');
const owner = adminUserId();
const other = makeUser({ email: 'gov-other@test.local' });
const deployment = createIntuneDeployment({ name: 'Gov Test App', visibility: 'shared', status: 'ready' }, owner);
test('approval lifecycle: pending -> approved -> executed', () => {
const request = createChangeRequest({ deploymentId: deployment.id, connectionId: null, action: 'publish', requestedBy: owner, requesterEmail: 'a@b.c' });
assert.equal(request.status, 'pending');
assert.equal(findPendingRequest(deployment.id, 'publish').id, request.id);
assert.equal(findApprovedRequest(deployment.id, 'publish'), null);
const approved = decideChangeRequest(request.id, { status: 'approved', approverUserId: owner, approverEmail: 'admin@b.c' });
assert.equal(approved.status, 'approved');
assert.equal(findApprovedRequest(deployment.id, 'publish').id, request.id);
markRequestExecuted(request.id);
assert.equal(findApprovedRequest(deployment.id, 'publish'), null, 'executed request is no longer approved');
});
test('deciding an already-decided request returns null', () => {
const request = createChangeRequest({ deploymentId: deployment.id, action: 'assign', requestedBy: owner });
decideChangeRequest(request.id, { status: 'rejected', approverUserId: owner });
assert.equal(decideChangeRequest(request.id, { status: 'approved', approverUserId: owner }), null);
});
test('change requests are scoped to visible deployments for non-admins', () => {
createChangeRequest({ deploymentId: deployment.id, action: 'relationships', requestedBy: owner });
// deployment is shared, so a non-admin still sees its requests
assert.ok(listChangeRequests(other, { isAdmin: false }).some((r) => r.deploymentId === deployment.id));
});
test('reporting overview aggregates deployment status and install states', () => {
const ts = new Date().toISOString();
const insert = db.prepare(`
INSERT INTO intune_deployment_statuses (id, deployment_id, graph_app_id, scope, install_state, error_code, created_at, updated_at)
VALUES (?, ?, 'app-1', 'device', ?, ?, ?, ?)
`);
insert.run('ids_1', deployment.id, 'failed', '0x87D1041C', ts, ts);
insert.run('ids_2', deployment.id, 'installed', '', ts, ts);
insert.run('ids_3', deployment.id, 'failed', '0x87D1041C', ts, ts);
const overview = intuneReportingOverview(owner, false);
const failed = overview.installStateCounts.find((row) => row.state === 'failed');
assert.equal(failed.count, 2);
assert.ok(overview.deploymentsByStatus.some((row) => row.status === 'ready'));
assert.equal(overview.topErrors[0].code, '0x87D1041C');
assert.equal(overview.topErrors[0].count, 2);
});
test('reporting overview includes soak timers and a write-action trend', () => {
const soaking = createIntuneDeployment({ name: 'Soak Report', visibility: 'shared', autoPromoteAfterHours: 24, autoPromoteRing: 'Broad' }, owner);
db.prepare('UPDATE intune_deployments SET soak_started_at = ? WHERE id = ?')
.run(new Date(Date.now() - 3_600_000).toISOString(), soaking.id);
recordGraphAudit({ deploymentId: soaking.id, actorUserId: owner, action: 'win32app.publish', status: 'success', message: 'ok' });
const overview = intuneReportingOverview(owner, false);
const timer = overview.soakTimers.find((row) => row.id === soaking.id);
assert.ok(timer, 'soak timer present');
assert.ok(timer.hoursRemaining > 22 && timer.hoursRemaining <= 24, `~23h remaining, got ${timer.hoursRemaining}`);
assert.ok(overview.auditTrend.some((row) => row.status === 'success' && row.count >= 1));
});

View File

@@ -0,0 +1,44 @@
import './setup.mjs';
import test from 'node:test';
import assert from 'node:assert/strict';
import { load } from './setup.mjs';
const { isAllowedGraphHost, isAllowedAuthorityHost } = await load('../data/graphHosts.js');
const { graphConnectionSchema } = await load('../forms/schemas.js');
test('allow-list accepts Microsoft cloud hosts', () => {
assert.ok(isAllowedGraphHost('https://graph.microsoft.com'));
assert.ok(isAllowedGraphHost('https://graph.microsoft.us'));
assert.ok(isAllowedAuthorityHost('https://login.microsoftonline.com'));
});
test('allow-list rejects non-Microsoft and internal hosts', () => {
assert.ok(!isAllowedGraphHost('https://graph.microsoft.com.evil.com'));
assert.ok(!isAllowedGraphHost('http://169.254.169.254'));
assert.ok(!isAllowedAuthorityHost('https://internal.corp.local'));
assert.ok(!isAllowedGraphHost('https://graph.microsoft.com:8080@evil.com'));
});
test('graph connection schema rejects SSRF endpoints', () => {
const result = graphConnectionSchema.safeParse({
name: 'Bad',
tenantId: 'contoso',
clientId: 'abc',
clientSecret: 'shh',
graphBaseUrl: 'http://169.254.169.254',
authorityHost: 'https://login.microsoftonline.com'
});
assert.equal(result.success, false);
});
test('graph connection schema accepts a valid Microsoft endpoint', () => {
const result = graphConnectionSchema.safeParse({
name: 'Good',
tenantId: 'contoso',
clientId: 'abc',
clientSecret: 'shh',
graphBaseUrl: 'https://graph.microsoft.com',
authorityHost: 'https://login.microsoftonline.com'
});
assert.equal(result.success, true);
});

View File

@@ -0,0 +1,33 @@
import { adminUserId, makeUser, load } from './setup.mjs';
import test from 'node:test';
import assert from 'node:assert/strict';
const { createHost, listHosts, updateHost, deleteHost, filterVisibleHostIds } = await load('../models/hostModel.js');
const owner = adminUserId();
const other = makeUser({ email: 'other-host@test.local' });
test('personal hosts are visible only to their owner', () => {
const host = createHost({ name: 'Private', address: 'priv.local', osFamily: 'windows', transport: 'winrm', tags: [], visibility: 'personal' }, owner);
assert.ok(listHosts(owner).some((h) => h.id === host.id));
assert.ok(!listHosts(other).some((h) => h.id === host.id));
});
test('shared hosts are visible to everyone', () => {
const host = createHost({ name: 'Shared', address: 'shared.local', osFamily: 'windows', transport: 'winrm', tags: [], visibility: 'shared' }, owner);
assert.ok(listHosts(other).some((h) => h.id === host.id));
});
test('a non-owner cannot update or delete a personal host', () => {
const host = createHost({ name: 'Locked', address: 'locked.local', osFamily: 'windows', transport: 'winrm', tags: [], visibility: 'personal' }, owner);
assert.equal(updateHost(host.id, { name: 'Hacked' }, other), null);
deleteHost(host.id, other);
assert.ok(listHosts(owner).some((h) => h.id === host.id), 'host should still exist after foreign delete attempt');
});
test('filterVisibleHostIds drops hosts the user cannot see', () => {
const priv = createHost({ name: 'OwnerOnly', address: 'owneronly.local', osFamily: 'windows', transport: 'winrm', tags: [], visibility: 'personal' }, owner);
const shared = createHost({ name: 'Everyone', address: 'everyone.local', osFamily: 'windows', transport: 'winrm', tags: [], visibility: 'shared' }, owner);
const visibleToOther = filterVisibleHostIds([priv.id, shared.id], other);
assert.deepEqual(visibleToOther, [shared.id]);
});

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

View File

@@ -0,0 +1,107 @@
import './setup.mjs';
import test from 'node:test';
import assert from 'node:assert/strict';
import { load } from './setup.mjs';
const { parseIntunewin } = await load('../services/intunewinParser.js');
// --- Minimal stored-method (no compression) ZIP builder for fixtures --------
function crc32(buf) {
let crc = 0xffffffff;
for (let i = 0; i < buf.length; i++) {
crc ^= buf[i];
for (let j = 0; j < 8; j++) crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1));
}
return (crc ^ 0xffffffff) >>> 0;
}
function makeStoredZip(entries) {
const locals = [];
const centrals = [];
let offset = 0;
for (const { name, data } of entries) {
const nameBuf = Buffer.from(name, 'utf8');
const crc = crc32(data);
const local = Buffer.alloc(30);
local.writeUInt32LE(0x04034b50, 0);
local.writeUInt16LE(20, 4);
local.writeUInt16LE(0, 8); // method = stored
local.writeUInt32LE(crc, 14);
local.writeUInt32LE(data.length, 18);
local.writeUInt32LE(data.length, 22);
local.writeUInt16LE(nameBuf.length, 26);
const localRecord = Buffer.concat([local, nameBuf, data]);
const central = Buffer.alloc(46);
central.writeUInt32LE(0x02014b50, 0);
central.writeUInt16LE(20, 4);
central.writeUInt16LE(20, 6);
central.writeUInt16LE(0, 10); // method = stored
central.writeUInt32LE(crc, 16);
central.writeUInt32LE(data.length, 20);
central.writeUInt32LE(data.length, 24);
central.writeUInt16LE(nameBuf.length, 28);
central.writeUInt32LE(offset, 42);
centrals.push(Buffer.concat([central, nameBuf]));
locals.push(localRecord);
offset += localRecord.length;
}
const cd = Buffer.concat(centrals);
const localsBuf = Buffer.concat(locals);
const eocd = Buffer.alloc(22);
eocd.writeUInt32LE(0x06054b50, 0);
eocd.writeUInt16LE(entries.length, 8);
eocd.writeUInt16LE(entries.length, 10);
eocd.writeUInt32LE(cd.length, 12);
eocd.writeUInt32LE(localsBuf.length, 16);
return Buffer.concat([localsBuf, cd, eocd]);
}
const detectionXml = `<?xml version="1.0" encoding="utf-8"?>
<ApplicationInfo ToolVersion="1.8.6">
<Name>Contoso VPN</Name>
<UnencryptedContentSize>2048</UnencryptedContentSize>
<FileName>IntunePackage.intunewin</FileName>
<SetupFile>ContosoVpnSetup.exe</SetupFile>
<EncryptionInfo>
<EncryptionKey>QUJDRA==</EncryptionKey>
<MacKey>RUZHSA==</MacKey>
<InitializationVector>SUpLTA==</InitializationVector>
<Mac>TU5PUA==</Mac>
<ProfileIdentifier>ProfileVersion1</ProfileIdentifier>
<FileDigest>UVJTVA==</FileDigest>
<FileDigestAlgorithm>SHA256</FileDigestAlgorithm>
</EncryptionInfo>
</ApplicationInfo>`;
const encryptedPayload = Buffer.from('this-stands-in-for-the-encrypted-intunewin-bytes');
const fixture = makeStoredZip([
{ name: 'IntuneWinPackage/Metadata/Detection.xml', data: Buffer.from(detectionXml, 'utf8') },
{ name: 'IntuneWinPackage/Contents/IntunePackage.intunewin', data: encryptedPayload }
]);
test('parses setup file, sizes, and encrypted payload', () => {
const parsed = parseIntunewin(fixture);
assert.equal(parsed.setupFile, 'ContosoVpnSetup.exe');
assert.equal(parsed.unencryptedContentSize, 2048);
assert.equal(parsed.encryptedContentSize, encryptedPayload.length);
assert.ok(parsed.encryptedContent.equals(encryptedPayload));
});
test('maps EncryptionInfo to Graph fileEncryptionInfo', () => {
const { fileEncryptionInfo } = parseIntunewin(fixture);
assert.equal(fileEncryptionInfo.encryptionKey, 'QUJDRA==');
assert.equal(fileEncryptionInfo.macKey, 'RUZHSA==');
assert.equal(fileEncryptionInfo.initializationVector, 'SUpLTA==');
assert.equal(fileEncryptionInfo.mac, 'TU5PUA==');
assert.equal(fileEncryptionInfo.profileIdentifier, 'ProfileVersion1');
assert.equal(fileEncryptionInfo.fileDigest, 'UVJTVA==');
assert.equal(fileEncryptionInfo.fileDigestAlgorithm, 'SHA256');
});
test('rejects a non-.intunewin buffer', () => {
assert.throws(() => parseIntunewin(Buffer.from('not a zip at all')));
});

37
server/test/jobs.test.mjs Normal file
View File

@@ -0,0 +1,37 @@
import { db, makeUser, adminUserId, load } from './setup.mjs';
import test from 'node:test';
import assert from 'node:assert/strict';
const { listJobs, getJob } = await load('../models/jobModel.js');
const owner = makeUser({ email: 'job-owner@test.local' });
const stranger = makeUser({ email: 'job-stranger@test.local' });
const admin = adminUserId();
// Build a personal RunPlan owned by `owner` and a job they triggered, directly
// in SQL so the test never invokes PowerShell.
const ts = new Date().toISOString();
const scriptId = 'scr_job_test';
db.prepare(`INSERT INTO scripts (id, name, content, visibility, owner_user_id, version, created_at, updated_at)
VALUES (?, 'Job Test', 'Write-Output 1', 'personal', ?, 1, ?, ?)`).run(scriptId, owner, ts, ts);
const runplanId = 'run_job_test';
db.prepare(`INSERT INTO runplans (id, name, script_id, visibility, owner_user_id, parallel, created_at, updated_at)
VALUES (?, 'Job Test Plan', ?, 'personal', ?, 1, ?, ?)`).run(runplanId, scriptId, owner, ts, ts);
const jobId = 'job_test_1';
db.prepare(`INSERT INTO jobs (id, runplan_id, script_id, status, triggered_by, created_at)
VALUES (?, ?, ?, 'completed', ?, ?)`).run(jobId, runplanId, scriptId, owner, ts);
test('the triggering operator can list and read their job', () => {
assert.ok(listJobs(owner, false).some((j) => j.id === jobId));
assert.ok(getJob(jobId, owner, false));
});
test('an unrelated user cannot see a personal job', () => {
assert.ok(!listJobs(stranger, false).some((j) => j.id === jobId));
assert.equal(getJob(jobId, stranger, false), null);
});
test('admins can see any job', () => {
assert.ok(listJobs(admin, true).some((j) => j.id === jobId));
assert.ok(getJob(jobId, admin, true));
});

View File

@@ -0,0 +1,65 @@
import { makeUser, load } from './setup.mjs';
import test from 'node:test';
import assert from 'node:assert/strict';
const { createScript, cloneScript, scriptUsage } = await load('../models/libraryModel.js');
const { createRunPlan } = await load('../models/runPlanModel.js');
const { createIntuneDeployment } = await load('../models/psadtModel.js');
const owner = makeUser({ email: 'library-owner@test.local' });
test('scriptUsage reports linked RunPlans and Intune deployments', () => {
const script = createScript({
name: 'Usage Target.ps1',
description: 'Used by operational plans',
content: 'Write-Output usage',
visibility: 'personal'
}, owner);
const runplan = createRunPlan({
name: 'Usage RunPlan',
description: '',
scriptId: script.id,
visibility: 'personal',
parallel: true,
hostIds: []
}, owner);
const deployment = createIntuneDeployment({
name: 'Usage Intune Plan',
scriptId: script.id,
installCommand: 'Invoke-AppDeployToolkit.exe -DeploymentType Install',
uninstallCommand: 'Invoke-AppDeployToolkit.exe -DeploymentType Uninstall',
visibility: 'personal'
}, owner);
const usage = scriptUsage(script.id, owner);
assert.equal(usage.total, 2);
assert.deepEqual(usage.runplans.map((item) => item.id), [runplan.id]);
assert.deepEqual(usage.intuneDeployments.map((item) => item.id), [deployment.id]);
});
test('cloneScript creates an independent unassigned script copy', () => {
const script = createScript({
name: 'Clone Source.ps1',
description: 'Original',
content: 'Write-Output clone',
visibility: 'personal'
}, owner);
createRunPlan({
name: 'Original Only',
description: '',
scriptId: script.id,
visibility: 'personal',
parallel: true,
hostIds: []
}, owner);
const clone = cloneScript(script.id, { name: 'Clone Target.ps1' }, owner);
assert.notEqual(clone.id, script.id);
assert.equal(clone.name, 'Clone Target.ps1');
assert.equal(clone.content, script.content);
assert.equal(scriptUsage(clone.id, owner).total, 0);
assert.equal(scriptUsage(script.id, owner).total, 1);
});

View File

@@ -0,0 +1,80 @@
import './setup.mjs';
import test from 'node:test';
import assert from 'node:assert/strict';
import { load } from './setup.mjs';
const { analyzeInstaller, listInstallerTechnologies } = await load('../services/installerIntelService.js');
const { buildDetectionRule } = await load('../services/detectionRuleService.js');
// --- installer detection + silent switches --------------------------------
test('MSI is detected with high confidence and product-code uninstall', () => {
const result = analyzeInstaller({ fileName: 'ContosoVpn.msi', productCode: '{12345678-1234-1234-1234-123456789012}' });
assert.equal(result.confidence, 'high');
assert.equal(result.primary.id, 'msi');
assert.ok(result.primary.install.includes('/qn'));
assert.ok(result.primary.uninstall.includes('{12345678-1234-1234-1234-123456789012}'));
});
test('MSIX is detected from its extension', () => {
const result = analyzeInstaller({ fileName: 'App_1.0.0.0_x64.msixbundle' });
assert.equal(result.primary.id, 'msix');
assert.ok(result.primary.install.includes('Add-AppxProvisionedPackage'));
});
test('EXE with a name hint matches the technology at medium confidence', () => {
const result = analyzeInstaller({ fileName: 'app-nsis-setup.exe' });
assert.equal(result.confidence, 'medium');
assert.equal(result.primary.id, 'nsis');
assert.equal(result.primary.install, '"app-nsis-setup.exe" /S');
});
test('ambiguous EXE falls back to generic and offers candidates', () => {
const result = analyzeInstaller({ fileName: 'installer.exe' });
assert.equal(result.confidence, 'low');
assert.equal(result.primary.id, 'exe');
const ids = result.candidates.map((c) => c.id);
assert.ok(ids.includes('inno') && ids.includes('installshield'));
assert.equal(ids[ids.length - 1], 'exe', 'generic EXE is offered last');
});
test('unknown extension reports unknown', () => {
const result = analyzeInstaller({ fileName: 'notes.txt' });
assert.equal(result.confidence, 'unknown');
assert.equal(result.primary, null);
});
test('listInstallerTechnologies exposes the catalog', () => {
const list = listInstallerTechnologies();
assert.ok(list.find((t) => t.id === 'inno'));
assert.ok(list.every((t) => Array.isArray(t.extensions)));
});
// --- detection rule generation --------------------------------------------
test('MSI detection maps to a product-code rule', () => {
const rule = buildDetectionRule({ type: 'msi', productCode: '{abc}' });
assert.equal(rule.detectionType, 'msi-product-code');
assert.equal(rule.detectionRule, '{abc}');
});
test('file detection generates a versioned PowerShell script', () => {
const rule = buildDetectionRule({ type: 'file', path: 'C:\\Program Files\\App\\app.exe', version: '2.0.0' });
assert.equal(rule.detectionType, 'custom-script');
assert.ok(rule.detectionRule.includes('Test-Path'));
assert.ok(rule.detectionRule.includes("[version]'2.0.0'"));
assert.ok(rule.detectionRule.includes("Write-Output 'Detected'"));
});
test('registry detection with a value comparison', () => {
const rule = buildDetectionRule({ type: 'registry', keyPath: 'HKLM:\\SOFTWARE\\Contoso', valueName: 'Version', valueData: '2.0' });
assert.equal(rule.detectionType, 'custom-script');
assert.ok(rule.detectionRule.includes('HKLM:\\SOFTWARE\\Contoso'));
assert.ok(rule.detectionRule.includes("-eq '2.0'"));
});
test('detection generator validates required inputs', () => {
assert.throws(() => buildDetectionRule({ type: 'msi' }));
assert.throws(() => buildDetectionRule({ type: 'file' }));
assert.throws(() => buildDetectionRule({ type: 'bogus' }));
});

View File

@@ -0,0 +1,53 @@
import './setup.mjs';
import test from 'node:test';
import assert from 'node:assert/strict';
import { load } from './setup.mjs';
const { bumpVersion, cloneForNextVersion, setRingIntent } = await load('../services/intunePromotionService.js');
test('bumpVersion increments the trailing number', () => {
assert.equal(bumpVersion('1.2.3'), '1.2.4');
assert.equal(bumpVersion('126'), '127');
assert.equal(bumpVersion('3.1.0-x64'), '3.1.0-x65'); // bumps the last numeric run
assert.equal(bumpVersion(''), '1.0.0');
assert.equal(bumpVersion('stable'), 'stable.1');
});
test('cloneForNextVersion produces an unpublished next-version plan', () => {
const source = {
name: 'Contoso VPN 3.1.0',
profileId: 'p1', applicationId: 'app1', commandStyle: 'v4', installBehavior: 'system',
restartBehavior: 'return-code', uiMode: 'native-v4', requirements: { architecture: 'x64' },
installCommand: 'i', uninstallCommand: 'u', detectionType: 'msi-product-code', detectionRule: '{x}',
returnCodes: [{ code: 0, type: 'success' }], assignments: [{ ring: 'Pilot', intent: 'available' }],
relationships: [], graphAppId: 'live-app', status: 'published', visibility: 'shared'
};
const clone = cloneForNextVersion(source, { version: '3.2.0' });
assert.equal(clone.name, 'Contoso VPN 3.2.0');
assert.equal(clone.graphAppId, '');
assert.equal(clone.status, 'draft');
assert.equal(clone.applicationId, 'app1');
assert.equal(clone.detectionRule, '{x}');
assert.deepEqual(clone.assignments, source.assignments);
});
test('cloneForNextVersion auto-bumps the version from the name when not given', () => {
const clone = cloneForNextVersion({ name: 'Firefox 120.0', visibility: 'shared' }, {});
assert.equal(clone.name, 'Firefox 120.1');
});
test('setRingIntent promotes only the named ring and reports change', () => {
const assignments = [
{ ring: 'Pilot', intent: 'available' },
{ ring: 'Broad', intent: 'available' }
];
const result = setRingIntent(assignments, 'Broad', 'required');
assert.equal(result.changed, true);
assert.equal(result.assignments.find((a) => a.ring === 'Broad').intent, 'required');
assert.equal(result.assignments.find((a) => a.ring === 'Pilot').intent, 'available');
});
test('setRingIntent is a no-op for unknown rings', () => {
const result = setRingIntent([{ ring: 'Pilot', intent: 'available' }], 'Nope', 'required');
assert.equal(result.changed, false);
});

View File

@@ -0,0 +1,38 @@
import { db, load } from './setup.mjs';
import test from 'node:test';
import assert from 'node:assert/strict';
const { getSetting, getBooleanSetting, updateSettings, effectiveTrustedOrigins } = await load('../models/settingsModel.js');
test('unpinned settings seed as editable defaults', () => {
// No SERVER_FQDN env var is set in the test environment, so server_fqdn is a
// default-sourced, admin-editable setting.
const source = db.prepare('SELECT source FROM settings WHERE key = ?').get('server_fqdn')?.source;
assert.equal(source, 'default');
});
test('structural settings are env-pinned and locked', () => {
const source = db.prepare('SELECT source FROM settings WHERE key = ?').get('database_path')?.source;
assert.equal(source, 'env');
});
test('updateSettings persists editable keys but rejects env-pinned keys', () => {
updateSettings({ server_fqdn: 'https://posh.example.com', database_path: '/evil/path.sqlite' });
assert.equal(getSetting('server_fqdn'), 'https://posh.example.com');
// The env-pinned database_path must be unchanged.
assert.notEqual(getSetting('database_path'), '/evil/path.sqlite');
});
test('allow_script_execution is readable as an effective boolean', () => {
updateSettings({ allow_script_execution: 'false' });
assert.equal(getBooleanSetting('allow_script_execution', true), false);
updateSettings({ allow_script_execution: 'true' });
assert.equal(getBooleanSetting('allow_script_execution', false), true);
});
test('effectiveTrustedOrigins merges admin edits with built-in defaults', () => {
updateSettings({ trusted_origins: 'https://ops.example.com' });
const origins = effectiveTrustedOrigins();
assert.ok(origins.includes('https://ops.example.com'));
assert.ok(origins.includes('http://localhost:3000'));
});

42
server/test/setup.mjs Normal file
View File

@@ -0,0 +1,42 @@
// Shared test bootstrap. Sets up an isolated temp SQLite database and storage
// dirs BEFORE any application module (which reads config at import time) is
// loaded, then migrates + seeds a fresh schema. Import this first, then use
// `load()` to dynamically import the modules under test.
import os from 'node:os';
import path from 'node:path';
import fs from 'node:fs';
import crypto from 'node:crypto';
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'poshm-test-'));
process.env.NODE_ENV = 'test';
process.env.DATABASE_PATH = path.join(tmpRoot, 'test.sqlite');
process.env.LOG_DIR = path.join(tmpRoot, 'logs');
process.env.FILE_LOCKER_DIR = path.join(tmpRoot, 'filelocker');
process.env.JWT_SECRET = `test-${crypto.randomBytes(8).toString('hex')}`;
process.env.CREDENTIAL_STORE_KEY = `cred-${crypto.randomBytes(8).toString('hex')}`;
// Leave entra/origin/allow_script_execution env unset so tests exercise the
// unpinned editable-default code paths. Tests never invoke PowerShell.
const dbModule = await import('../db.js');
dbModule.migrate();
dbModule.seed();
export const db = dbModule.db;
export async function load(modulePath) {
return import(modulePath);
}
export function adminUserId() {
return db.prepare('SELECT id FROM users WHERE role = ? LIMIT 1').get('admin').id;
}
export function makeUser({ email, role = 'user' } = {}) {
const id = `usr_test_${crypto.randomBytes(5).toString('hex')}`;
db.prepare(`
INSERT INTO users (id, email, display_name, password_hash, auth_provider, role, created_at)
VALUES (?, ?, ?, NULL, 'local', ?, ?)
`).run(id, email || `${id}@test.local`, id, role, new Date().toISOString());
return id;
}