mattermost-mobile/app/managers/security_manager/event_handlers.test.ts
Elias Nahum 44eb76bed7
feat(iOS): Add Microsoft Intune MAM integration with multi-server support (#9312)
* refactor: implement custom ExpoImage wrapper for cache control

Add ExpoImage component with automatic cacheKey/cachePath management and replace all expo-image imports across the app

* refactor(ios): convert Gekidou to CocoaPods

Migrate from Swift Package Manager to CocoaPods, add Keychain write operations, refactor notification handler to remove react-native-notifications headers, and upgrade Swift to 5.0

* npm audit

* update fastlane

* feat(ci): integrate Intune MAM for enterprise builds with strict OSS protection

Add Intune submodule, CI actions, Fastlane configuration, developer scripts, pre-commit hooks, and validation workflows to enable internal MAM builds while protecting OSS repository

* fix tests by mocking @mattermost/intune

* feat: implement Intune MAM integration with comprehensive security enforcement

Add IntuneManager, refactor SecurityManager/SessionManager for MAM policies, implement native OIDC auth flow, add biometric enforcement, conditional launch blocking, and file protection controls

* fix alerts when no server database is present

* Unify cache strategy

* fix emit config changed after it was stored in the db

* Handle Mid-Session Enrollment Detection

* fix ADALLogOverrideDisabled missing in Fastfile

* fix flow for initial enrollment

* fix and add unit tests

* enable Intune configuration for PR and beta builds, CLIENT_ID should be changed before actual release

* Update intune submodule with addressed feedback

* fix validate-intune-clean workflow

* feat(intune): add comprehensive error handling and SAML+Entra support

Add production-ready error handling for native Entra authentication with
user-friendly i18n messages, comprehensive test coverage, and support for
Entra login when server requires SAML.

* update i18n

* update intune submodule

* update build-pr token

* fix race condition between server auth and intune enrollment

* fix CI workflow to build with intune

* use deploy key for intune submodule

* set the config directly in the submodule .git

* debug injection

* try setting GIT_SSH_COMMAND

* remove action debug

* fix server url input

* match pod cache with intune hash

* Fastfile and envs

* have workflows check for intune/.git

* have ci cache intune frameworks as well

* update Fastlane to set no-cache to artifacts uploaded

* fix s3 upload

* fix pblist template

* Attempt to remove the cache control for PR uploads to s3

* use hash from commit for S3 path

* Implement crash-resilient selective wipe with automatic retry and add removeInternetPassword to Gekidou Keychain

* Fix surface errors from intune login

* fix postinstall scripts

* use cacheKey for draft md images

* remove unnecessary double await during test

* Have isMinimumLicenseTier accept valid license sku tier as target

* Add missing Auth error messages

* remove the last period for intune errors in i18n

* do not call unenroll with wipe on manual logout

* Fix tests and Intune error messages

* do not filter any SSO type regardless of which is used for Intune

* fix 412 to not retry

* fix tests, app logs sharing and share_extension avatar cache

* apply setScreenCapturePolicy on license change

Co-authored-by: Eva Sarafianou <eva.sarafianou@mattermost.com>

* re-apply screen capture on enrollment

Co-authored-by: Eva Sarafianou <eva.sarafianou@mattermost.com>

* use userData from intunr login and prevent getMe

Co-authored-by: Eva Sarafianou <eva.sarafianou@mattermost.com>

* Check for Biometrics and Jailbreak as we used to

---------

Co-authored-by: Eva Sarafianou <eva.sarafianou@mattermost.com>
2025-12-10 13:07:28 +02:00

835 lines
39 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/* eslint-disable max-lines */
import Emm from '@mattermost/react-native-emm';
import {isRootedExperimentalAsync} from 'expo-device';
import {type AppStateStatus} from 'react-native';
import DatabaseManager from '@database/manager';
import {getServerCredentials} from '@init/credentials';
import IntuneManager from '@managers/intune_manager';
import {
IntuneConditionalLaunchBlockedReasons,
type IntuneAuthRequiredEvent,
type IntuneConditionalLaunchBlockedEvent,
type IntuneEnrollmentChangedEvent,
type IntuneIdentitySwitchRequiredEvent,
type IntunePolicy,
type IntunePolicyChangedEvent,
type IntuneWipeRequestedEvent,
} from '@managers/intune_manager/types';
import TestHelper from '@test/test_helper';
import * as alerts from '@utils/alerts';
import {toMilliseconds} from '@utils/datetime';
import SecurityManager from '.';
jest.mock('@mattermost/react-native-emm', () => ({
isDeviceSecured: jest.fn(),
authenticate: jest.fn(),
openSecuritySettings: jest.fn(),
exitApp: jest.fn(),
enableBlurScreen: jest.fn(),
applyBlurEffect: jest.fn(),
removeBlurEffect: jest.fn(),
addListener: jest.fn(),
setAppGroupId: jest.fn(),
getManagedConfig: jest.fn(() => ({})),
}));
jest.mock('expo-device', () => ({
isRootedExperimentalAsync: jest.fn(),
}));
jest.mock('@actions/local/session', () => ({terminateSession: jest.fn(() => Promise.resolve())}));
jest.mock('@actions/local/user', () => ({getCurrentUserLocale: jest.fn(() => Promise.resolve('en'))}));
jest.mock('@actions/remote/session', () => ({
logout: jest.fn(() => Promise.resolve()),
}));
jest.mock('@utils/log', () => ({
logError: jest.fn(),
logDebug: jest.fn(),
}));
jest.mock('@utils/datetime', () => ({toMilliseconds: jest.fn(() => 25000)}));
jest.mock('@init/credentials', () => ({getServerCredentials: jest.fn().mockResolvedValue({token: 'token'})}));
jest.mock('@database/manager', () => ({
getActiveServerUrl: jest.fn(),
getServerDatabaseAndOperator: jest.fn(),
}));
describe('SecurityManager - Event Handlers', () => {
beforeEach(() => {
jest.clearAllMocks();
SecurityManager.initialized = false;
SecurityManager.serverConfig = {};
SecurityManager.activeServer = undefined;
});
describe('onAppStateChange', () => {
test('should handle app state changes', async () => {
await SecurityManager.addServer('server-8', {MobileEnableBiometrics: 'true'} as SecurityClientConfig);
await SecurityManager.setActiveServer({serverUrl: 'server-8'});
await SecurityManager.onAppStateChange('background' as AppStateStatus);
expect(SecurityManager.backgroundSince).toBeGreaterThan(0);
await SecurityManager.onAppStateChange('active' as AppStateStatus);
expect(SecurityManager.backgroundSince).toBe(0);
});
test('should call biometric authentication app state changes', async () => {
const authenticateWithBiometrics = jest.spyOn(SecurityManager, 'authenticateWithBiometrics');
jest.mocked(isRootedExperimentalAsync).mockResolvedValue(false);
await SecurityManager.addServer('server-8', {MobileEnableBiometrics: 'true'} as SecurityClientConfig);
await SecurityManager.setActiveServer({serverUrl: 'server-8'});
SecurityManager.onAppStateChange('background' as AppStateStatus);
SecurityManager.backgroundSince = Date.now() - toMilliseconds({minutes: 5, seconds: 1});
SecurityManager.onAppStateChange('active' as AppStateStatus);
await TestHelper.wait(300);
expect(authenticateWithBiometrics).toHaveBeenCalledWith('server-8');
});
});
describe('Intune Event Handlers', () => {
const serverUrl1 = 'https://test1.server.com';
const serverUrl2 = 'https://test2.server.com';
const mockPolicy: IntunePolicy = {
isPINRequired: true,
isContactSyncAllowed: false,
isWidgetContentSyncAllowed: false,
isSpotlightIndexingAllowed: false,
areSiriIntentsAllowed: false,
areAppIntentsAllowed: false,
isAppSharingAllowed: false,
shouldFileProviderEncryptFiles: true,
isManagedBrowserRequired: false,
isFileEncryptionRequired: true,
isScreenCaptureAllowed: false,
notificationPolicy: 0,
allowedSaveLocations: {
Other: false,
OneDriveForBusiness: true,
SharePoint: true,
LocalDrive: false,
PhotoLibrary: false,
CameraRoll: false,
FilesApp: false,
iCloudDrive: false,
},
allowedOpenLocations: 0,
};
beforeEach(async () => {
await SecurityManager.init();
await SecurityManager.addServer(serverUrl1, {SiteName: 'Test Server 1'} as SecurityClientConfig);
await SecurityManager.addServer(serverUrl2, {SiteName: 'Test Server 2'} as SecurityClientConfig);
});
afterEach(() => {
SecurityManager.cleanup();
});
describe('onIntunePolicyChanged', () => {
test('should skip if not initialized', () => {
SecurityManager.cleanup();
const event: IntunePolicyChangedEvent = {
oid: 'object-id',
changed: true,
removed: false,
policy: mockPolicy,
serverUrls: [serverUrl1],
};
SecurityManager.onIntunePolicyChanged(event);
// After cleanup, handler should return early without modifying config
// Server still exists with null policy (from addServer)
const config = SecurityManager.getServerConfig(serverUrl1);
expect(config?.intunePolicy).toBeUndefined();
});
test('should update policy for affected servers when policy changed', () => {
const event: IntunePolicyChangedEvent = {
oid: 'object-id',
changed: true,
removed: false,
policy: mockPolicy,
serverUrls: [serverUrl1, serverUrl2],
};
SecurityManager.onIntunePolicyChanged(event);
const config1 = SecurityManager.getServerConfig(serverUrl1);
const config2 = SecurityManager.getServerConfig(serverUrl2);
expect(config1?.intunePolicy).toEqual(mockPolicy);
expect(config2?.intunePolicy).toEqual(mockPolicy);
});
test('should remove policy when policy removed', () => {
// First set a policy
SecurityManager.addServer(serverUrl1, {} as SecurityClientConfig, false, mockPolicy);
const event: IntunePolicyChangedEvent = {
oid: 'object-id',
changed: false,
removed: true,
policy: null,
serverUrls: [serverUrl1],
};
SecurityManager.onIntunePolicyChanged(event);
const config = SecurityManager.getServerConfig(serverUrl1);
expect(config?.intunePolicy).toBeNull();
});
test('should re-apply screen capture policy when active server affected', async () => {
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: true, skipJailbreakCheck: true, skipBiometricCheck: true}});
const setScreenCaptureSpy = jest.spyOn(SecurityManager, 'setScreenCapturePolicy');
const event: IntunePolicyChangedEvent = {
oid: 'object-id',
changed: true,
removed: false,
policy: mockPolicy,
serverUrls: [serverUrl1],
};
SecurityManager.onIntunePolicyChanged(event);
expect(setScreenCaptureSpy).toHaveBeenCalledWith(serverUrl1);
setScreenCaptureSpy.mockRestore();
});
test('should not re-apply when active server not affected', async () => {
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: true, skipJailbreakCheck: true, skipBiometricCheck: true}});
const setScreenCaptureSpy = jest.spyOn(SecurityManager, 'setScreenCapturePolicy');
setScreenCaptureSpy.mockClear();
const event: IntunePolicyChangedEvent = {
oid: 'object-id',
changed: true,
removed: false,
policy: mockPolicy,
serverUrls: [serverUrl2],
};
SecurityManager.onIntunePolicyChanged(event);
// Should not be called again for serverUrl1
expect(setScreenCaptureSpy).not.toHaveBeenCalled();
setScreenCaptureSpy.mockRestore();
});
test('should handle multiple servers in event', () => {
const event: IntunePolicyChangedEvent = {
oid: 'object-id',
changed: true,
removed: false,
policy: mockPolicy,
serverUrls: [serverUrl1, serverUrl2],
};
SecurityManager.onIntunePolicyChanged(event);
const config1 = SecurityManager.getServerConfig(serverUrl1);
const config2 = SecurityManager.getServerConfig(serverUrl2);
expect(config1?.intunePolicy).toEqual(mockPolicy);
expect(config2?.intunePolicy).toEqual(mockPolicy);
});
});
describe('onEnrollmentChanged', () => {
test('should call handleEnrollmentSuccess when enrolled', async () => {
const handleSuccessSpy = jest.spyOn(SecurityManager as any, 'handleEnrollmentSuccess');
const event: IntuneEnrollmentChangedEvent = {
oid: 'object-id',
enrolled: true,
reason: 'user_enrolled',
serverUrls: [serverUrl1],
};
SecurityManager.onEnrollmentChanged(event);
await TestHelper.wait(10);
expect(handleSuccessSpy).toHaveBeenCalledWith([serverUrl1]);
handleSuccessSpy.mockRestore();
});
test('should call handleUnenrollment when not enrolled', async () => {
const handleUnenrollmentSpy = jest.spyOn(SecurityManager as any, 'handleUnenrollment');
const event: IntuneEnrollmentChangedEvent = {
oid: 'object-id',
enrolled: false,
reason: 'user_unenrolled',
serverUrls: [serverUrl1],
};
SecurityManager.onEnrollmentChanged(event);
await TestHelper.wait(10);
expect(handleUnenrollmentSpy).toHaveBeenCalledWith([serverUrl1], 'user_unenrolled');
handleUnenrollmentSpy.mockRestore();
});
});
describe('onWipeRequested', () => {
beforeEach(() => {
jest.mocked(getServerCredentials).mockResolvedValue({serverUrl: serverUrl1, userId: 'user-id', token: 'token'});
});
test('should wipe background servers first, then active server', async () => {
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: true, skipJailbreakCheck: true, skipBiometricCheck: true}});
const performWipeSpy = jest.spyOn(SecurityManager as any, 'performSelectiveWipe').mockResolvedValue(undefined);
const event: IntuneWipeRequestedEvent = {
oid: 'object-id',
serverUrls: [serverUrl1, serverUrl2],
};
await SecurityManager.onWipeRequested(event);
// Background server wiped first with skipEvents=true
expect(performWipeSpy).toHaveBeenCalledWith(serverUrl2, true);
// Active server wiped last with skipEvents=false
expect(performWipeSpy).toHaveBeenCalledWith(serverUrl1, false);
performWipeSpy.mockRestore();
});
test('should proceed with wipe even without credentials (already deleted by native)', async () => {
jest.mocked(getServerCredentials).mockResolvedValue(null);
const performWipeSpy = jest.spyOn(SecurityManager as any, 'performSelectiveWipe').mockResolvedValue(true);
const event: IntuneWipeRequestedEvent = {
oid: 'object-id',
serverUrls: [serverUrl1],
};
await SecurityManager.onWipeRequested(event);
// Should still call wipe to clean up database (credentials already deleted by native)
expect(performWipeSpy).toHaveBeenCalledWith(serverUrl1, true);
performWipeSpy.mockRestore();
});
test('should remove active server after wipe', async () => {
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: true, skipJailbreakCheck: true, skipBiometricCheck: true}});
jest.spyOn(SecurityManager as any, 'performSelectiveWipe').mockResolvedValue(undefined);
const removeServerSpy = jest.spyOn(SecurityManager, 'removeServer');
const event: IntuneWipeRequestedEvent = {
oid: 'object-id',
serverUrls: [serverUrl1],
};
await SecurityManager.onWipeRequested(event);
expect(removeServerSpy).toHaveBeenCalledWith(serverUrl1);
removeServerSpy.mockRestore();
});
});
describe('onAuthRequired', () => {
test('should apply blur screen and trigger wipe', async () => {
const showAlertSpy = jest.spyOn(alerts, 'showAuthenticationRequiredAlert').mockImplementation(async (_reason, _locale, onDismiss) => {
if (onDismiss) {
onDismiss();
}
});
const performWipeSpy = jest.spyOn(SecurityManager as any, 'performSelectiveWipe').mockResolvedValue(undefined);
const event: IntuneAuthRequiredEvent = {
oid: 'object-id',
serverUrls: [serverUrl1],
};
await SecurityManager.onAuthRequired(event);
expect(Emm.applyBlurEffect).toHaveBeenCalled();
expect(showAlertSpy).toHaveBeenCalled();
await TestHelper.wait(10);
expect(performWipeSpy).toHaveBeenCalledWith(serverUrl1, true);
expect(Emm.removeBlurEffect).toHaveBeenCalled();
showAlertSpy.mockRestore();
performWipeSpy.mockRestore();
});
test('should remove blur screen when alert dismissed', async () => {
const showAlertSpy = jest.spyOn(alerts, 'showAuthenticationRequiredAlert').mockImplementation(async (_reason, _locale, onDismiss) => {
if (onDismiss) {
onDismiss();
}
});
const event: IntuneAuthRequiredEvent = {
oid: 'object-id',
serverUrls: [serverUrl1],
};
await SecurityManager.onAuthRequired(event);
expect(Emm.applyBlurEffect).toHaveBeenCalled();
await TestHelper.wait(10);
expect(Emm.removeBlurEffect).toHaveBeenCalled();
showAlertSpy.mockRestore();
});
});
describe('onConditionalLaunchBlocked', () => {
test('should trigger wipe when launch blocked', async () => {
const showAlertSpy = jest.spyOn(alerts, 'showConditionalAccessAlert').mockImplementation(async (_locale, callback) => {
if (callback) {
callback();
}
});
const performWipeSpy = jest.spyOn(SecurityManager as any, 'performSelectiveWipe').mockResolvedValue(undefined);
const event: IntuneConditionalLaunchBlockedEvent = {
oid: 'object-id',
reason: IntuneConditionalLaunchBlockedReasons.LAUNCH_BLOCKED,
serverUrls: [serverUrl1],
};
await SecurityManager.onConditionalLaunchBlocked(event);
expect(Emm.applyBlurEffect).toHaveBeenCalled();
expect(showAlertSpy).toHaveBeenCalled();
await TestHelper.wait(10);
expect(performWipeSpy).toHaveBeenCalledWith(serverUrl1, true);
showAlertSpy.mockRestore();
performWipeSpy.mockRestore();
});
test('should remove blur screen when launch blocked alert dismissed', async () => {
const showAlertSpy = jest.spyOn(alerts, 'showConditionalAccessAlert').mockImplementation(async (_locale, callback) => {
if (callback) {
callback();
}
});
const event: IntuneConditionalLaunchBlockedEvent = {
oid: 'object-id',
reason: IntuneConditionalLaunchBlockedReasons.LAUNCH_BLOCKED,
serverUrls: [serverUrl1],
};
await SecurityManager.onConditionalLaunchBlocked(event);
await TestHelper.wait(10);
expect(Emm.removeBlurEffect).toHaveBeenCalled();
showAlertSpy.mockRestore();
});
test('should retry authentication when launch canceled', async () => {
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: true, skipJailbreakCheck: true, skipBiometricCheck: true}});
const showAlertSpy = jest.spyOn(alerts, 'showBiometricFailureAlertForOrganization').mockImplementation(async (_server, _locale, retryCallback) => {
if (retryCallback) {
await retryCallback();
}
});
const setIdentitySpy = jest.spyOn(IntuneManager, 'setCurrentIdentity').mockResolvedValue(undefined);
const event: IntuneConditionalLaunchBlockedEvent = {
oid: 'object-id',
reason: IntuneConditionalLaunchBlockedReasons.LAUNCH_CANCELED,
serverUrls: [serverUrl1],
};
await SecurityManager.onConditionalLaunchBlocked(event);
expect(showAlertSpy).toHaveBeenCalled();
expect(setIdentitySpy).toHaveBeenCalledWith(serverUrl1);
showAlertSpy.mockRestore();
setIdentitySpy.mockRestore();
});
test('should remove blur screen after retry', async () => {
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: true, skipJailbreakCheck: true, skipBiometricCheck: true}});
const showAlertSpy = jest.spyOn(alerts, 'showBiometricFailureAlertForOrganization').mockImplementation(async (_server, _locale, retryCallback) => {
if (retryCallback) {
await retryCallback();
}
});
jest.spyOn(IntuneManager, 'setCurrentIdentity').mockResolvedValue(undefined);
const event: IntuneConditionalLaunchBlockedEvent = {
oid: 'object-id',
reason: IntuneConditionalLaunchBlockedReasons.LAUNCH_CANCELED,
serverUrls: [serverUrl1],
};
await SecurityManager.onConditionalLaunchBlocked(event);
expect(Emm.removeBlurEffect).toHaveBeenCalled();
showAlertSpy.mockRestore();
});
});
describe('onIdentitySwitchRequired', () => {
test('should trigger wipe and show alert', async () => {
const showAlertSpy = jest.spyOn(alerts, 'showIdentitySwitchRequiredAlert').mockResolvedValue(undefined);
const performWipeSpy = jest.spyOn(SecurityManager as any, 'performSelectiveWipe').mockResolvedValue(undefined);
const event: IntuneIdentitySwitchRequiredEvent = {
oid: 'object-id',
serverUrls: [serverUrl1],
reason: 'new_identity_required',
};
await SecurityManager.onIdentitySwitchRequired(event);
expect(showAlertSpy).toHaveBeenCalled();
await TestHelper.wait(10);
expect(performWipeSpy).toHaveBeenCalledWith(serverUrl1, true);
showAlertSpy.mockRestore();
performWipeSpy.mockRestore();
});
});
describe('handleEnrollmentSuccess', () => {
beforeEach(() => {
jest.mocked(DatabaseManager.getActiveServerUrl).mockResolvedValue(serverUrl1);
jest.mocked(IntuneManager.getPolicy).mockResolvedValue(mockPolicy);
});
test('should fetch and update policy for all affected servers', async () => {
await (SecurityManager as any).handleEnrollmentSuccess([serverUrl1, serverUrl2]);
const config1 = SecurityManager.getServerConfig(serverUrl1);
const config2 = SecurityManager.getServerConfig(serverUrl2);
expect(config1?.intunePolicy).toEqual(mockPolicy);
expect(config2?.intunePolicy).toEqual(mockPolicy);
});
test('should set current identity if active server is affected', async () => {
await (SecurityManager as any).handleEnrollmentSuccess([serverUrl1, serverUrl2]);
expect(IntuneManager.setCurrentIdentity).toHaveBeenCalledWith(serverUrl1);
});
test('should re-apply screen capture policy for active server', async () => {
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: true, skipJailbreakCheck: true, skipBiometricCheck: true}});
const setScreenCaptureSpy = jest.spyOn(SecurityManager, 'setScreenCapturePolicy');
await (SecurityManager as any).handleEnrollmentSuccess([serverUrl1]);
expect(setScreenCaptureSpy).toHaveBeenCalledWith(serverUrl1);
setScreenCaptureSpy.mockRestore();
});
test('should not set current identity if active server not affected', async () => {
jest.mocked(IntuneManager.setCurrentIdentity).mockClear();
await (SecurityManager as any).handleEnrollmentSuccess([serverUrl2]);
expect(IntuneManager.setCurrentIdentity).not.toHaveBeenCalled();
});
});
describe('handleUnenrollment', () => {
beforeEach(async () => {
await SecurityManager.addServer(serverUrl1, {} as SecurityClientConfig, false, mockPolicy);
await SecurityManager.addServer(serverUrl2, {} as SecurityClientConfig, false, mockPolicy);
jest.mocked(DatabaseManager.getActiveServerUrl).mockResolvedValue(serverUrl1);
});
test('should remove intunePolicy from all affected servers', async () => {
await (SecurityManager as any).handleUnenrollment([serverUrl1, serverUrl2], 'admin_unenrolled');
const config1 = SecurityManager.getServerConfig(serverUrl1);
const config2 = SecurityManager.getServerConfig(serverUrl2);
expect(config1?.intunePolicy).toBeNull();
expect(config2?.intunePolicy).toBeNull();
});
test('should re-apply screen capture policy if active server affected', async () => {
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: true, skipJailbreakCheck: true, skipBiometricCheck: true}});
const setScreenCaptureSpy = jest.spyOn(SecurityManager, 'setScreenCapturePolicy');
await (SecurityManager as any).handleUnenrollment([serverUrl1], 'policy_removed');
expect(setScreenCaptureSpy).toHaveBeenCalledWith(serverUrl1);
setScreenCaptureSpy.mockRestore();
});
test('should not re-apply if active server not affected', async () => {
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: true, skipJailbreakCheck: true, skipBiometricCheck: true}});
const setScreenCaptureSpy = jest.spyOn(SecurityManager, 'setScreenCapturePolicy');
setScreenCaptureSpy.mockClear();
await (SecurityManager as any).handleUnenrollment([serverUrl2]);
expect(setScreenCaptureSpy).not.toHaveBeenCalled();
setScreenCaptureSpy.mockRestore();
});
});
describe('onConfigChanged', () => {
const mockConfig = {
SiteName: 'Updated Server',
MobileEnableBiometrics: 'true',
MobileJailbreakProtection: 'true',
MobilePreventScreenCapture: 'true',
} as SecurityClientConfig;
beforeEach(() => {
jest.mocked(IntuneManager.getPolicy).mockResolvedValue(mockPolicy);
});
test('should update server config when config changes', async () => {
await SecurityManager.onConfigChanged({serverUrl: serverUrl1, config: mockConfig});
const config = SecurityManager.getServerConfig(serverUrl1);
expect(config?.siteName).toBe('Updated Server');
expect(config?.Biometrics).toBe(true);
expect(config?.JailbreakProtection).toBe(true);
expect(config?.PreventScreenCapture).toBe(true);
});
test('should fetch and store Intune policy when config changes', async () => {
await SecurityManager.onConfigChanged({serverUrl: serverUrl1, config: mockConfig});
const config = SecurityManager.getServerConfig(serverUrl1);
expect(config?.intunePolicy).toEqual(mockPolicy);
expect(IntuneManager.getPolicy).toHaveBeenCalledWith(serverUrl1);
});
test('should preserve existing config fields when updating', async () => {
await SecurityManager.addServer(serverUrl1, {} as SecurityClientConfig, true);
SecurityManager.serverConfig[serverUrl1].lastAccessed = 12345;
await SecurityManager.onConfigChanged({serverUrl: serverUrl1, config: mockConfig});
const config = SecurityManager.getServerConfig(serverUrl1);
expect(config?.lastAccessed).toBe(12345);
expect(config?.authenticated).toBe(true);
});
test('should trigger MAM enrollment check for active server', async () => {
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: true, skipJailbreakCheck: true, skipBiometricCheck: true}});
const ensureMAMSpy = jest.spyOn(SecurityManager, 'ensureMAMEnrollmentForActiveServer').mockResolvedValue(true);
await SecurityManager.onConfigChanged({serverUrl: serverUrl1, config: mockConfig});
expect(ensureMAMSpy).toHaveBeenCalledWith(serverUrl1);
ensureMAMSpy.mockRestore();
});
test('should apply screen capture policy for active server after enrollment check', async () => {
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: true, skipJailbreakCheck: true, skipBiometricCheck: true}});
jest.spyOn(SecurityManager, 'ensureMAMEnrollmentForActiveServer').mockResolvedValue(true);
const setScreenCaptureSpy = jest.spyOn(SecurityManager, 'setScreenCapturePolicy');
await SecurityManager.onConfigChanged({serverUrl: serverUrl1, config: mockConfig});
expect(setScreenCaptureSpy).toHaveBeenCalledWith(serverUrl1);
setScreenCaptureSpy.mockRestore();
});
test('should not trigger enrollment for non-active server', async () => {
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: true, skipJailbreakCheck: true, skipBiometricCheck: true}});
const ensureMAMSpy = jest.spyOn(SecurityManager, 'ensureMAMEnrollmentForActiveServer');
await SecurityManager.onConfigChanged({serverUrl: serverUrl2, config: mockConfig});
expect(ensureMAMSpy).not.toHaveBeenCalled();
ensureMAMSpy.mockRestore();
});
});
describe('onLicenseChanged', () => {
const mockLicense = {
IsLicensed: 'true',
SkuShortName: 'enterprise',
} as ClientLicense;
beforeEach(() => {
jest.mocked(IntuneManager.getPolicy).mockResolvedValue(mockPolicy);
});
test('should create server config if not exists', async () => {
await SecurityManager.onLicenseChanged({serverUrl: 'https://new-server.com', license: mockLicense});
const config = SecurityManager.getServerConfig('https://new-server.com');
expect(config).toBeDefined();
expect(config?.intunePolicy).toEqual(mockPolicy);
});
test('should fetch and update Intune policy when license changes', async () => {
await SecurityManager.onLicenseChanged({serverUrl: serverUrl1, license: mockLicense});
const config = SecurityManager.getServerConfig(serverUrl1);
expect(config?.intunePolicy).toEqual(mockPolicy);
expect(IntuneManager.getPolicy).toHaveBeenCalledWith(serverUrl1);
});
test('should trigger MAM enrollment check for active server', async () => {
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: true, skipJailbreakCheck: true, skipBiometricCheck: true}});
const ensureMAMSpy = jest.spyOn(SecurityManager, 'ensureMAMEnrollmentForActiveServer').mockResolvedValue(true);
await SecurityManager.onLicenseChanged({serverUrl: serverUrl1, license: mockLicense});
expect(ensureMAMSpy).toHaveBeenCalledWith(serverUrl1);
ensureMAMSpy.mockRestore();
});
test('should set current identity for active server after successful enrollment', async () => {
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: true, skipJailbreakCheck: true, skipBiometricCheck: true}});
jest.spyOn(SecurityManager, 'ensureMAMEnrollmentForActiveServer').mockResolvedValue(true);
await SecurityManager.onLicenseChanged({serverUrl: serverUrl1, license: mockLicense});
expect(IntuneManager.setCurrentIdentity).toHaveBeenCalledWith(serverUrl1);
});
test('should not set identity if enrollment fails', async () => {
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: true, skipJailbreakCheck: true, skipBiometricCheck: true}});
jest.spyOn(SecurityManager, 'ensureMAMEnrollmentForActiveServer').mockResolvedValue(false);
jest.mocked(IntuneManager.setCurrentIdentity).mockClear();
await SecurityManager.onLicenseChanged({serverUrl: serverUrl1, license: mockLicense});
expect(IntuneManager.setCurrentIdentity).not.toHaveBeenCalled();
});
test('should not trigger enrollment for non-active server', async () => {
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: true, skipJailbreakCheck: true, skipBiometricCheck: true}});
const ensureMAMSpy = jest.spyOn(SecurityManager, 'ensureMAMEnrollmentForActiveServer');
await SecurityManager.onLicenseChanged({serverUrl: serverUrl2, license: mockLicense});
expect(ensureMAMSpy).not.toHaveBeenCalled();
ensureMAMSpy.mockRestore();
});
});
describe('canSaveToLocation', () => {
test('should return true when no Intune policy exists', () => {
const result = SecurityManager.canSaveToLocation(serverUrl1, 'PhotoLibrary');
expect(result).toBe(true);
});
test('should return policy value when Intune policy exists', async () => {
await SecurityManager.addServer(serverUrl1, {} as SecurityClientConfig, false, mockPolicy);
const canSaveToOneDrive = SecurityManager.canSaveToLocation(serverUrl1, 'OneDriveForBusiness');
const canSaveToPhotoLibrary = SecurityManager.canSaveToLocation(serverUrl1, 'PhotoLibrary');
expect(canSaveToOneDrive).toBe(true);
expect(canSaveToPhotoLibrary).toBe(false);
});
test('should return false for restricted locations', async () => {
await SecurityManager.addServer(serverUrl1, {} as SecurityClientConfig, false, mockPolicy);
expect(SecurityManager.canSaveToLocation(serverUrl1, 'LocalDrive')).toBe(false);
expect(SecurityManager.canSaveToLocation(serverUrl1, 'CameraRoll')).toBe(false);
expect(SecurityManager.canSaveToLocation(serverUrl1, 'FilesApp')).toBe(false);
expect(SecurityManager.canSaveToLocation(serverUrl1, 'iCloudDrive')).toBe(false);
});
test('should return true for allowed locations', async () => {
await SecurityManager.addServer(serverUrl1, {} as SecurityClientConfig, false, mockPolicy);
expect(SecurityManager.canSaveToLocation(serverUrl1, 'OneDriveForBusiness')).toBe(true);
expect(SecurityManager.canSaveToLocation(serverUrl1, 'SharePoint')).toBe(true);
});
});
describe('setActiveServer options', () => {
beforeEach(async () => {
jest.mocked(IntuneManager.isIntuneMAMEnabledForServer).mockResolvedValue(true);
jest.mocked(IntuneManager.isManagedServer).mockResolvedValue(false);
jest.mocked(isRootedExperimentalAsync).mockResolvedValue(true);
jest.mocked(Emm.isDeviceSecured).mockResolvedValue(true);
jest.mocked(Emm.authenticate).mockResolvedValue(true);
});
test('should skip MAM enrollment check when skipMAMEnrollmentCheck is true', async () => {
const ensureMAMSpy = jest.spyOn(SecurityManager, 'ensureMAMEnrollmentForActiveServer');
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: true}});
expect(ensureMAMSpy).not.toHaveBeenCalled();
ensureMAMSpy.mockRestore();
});
test('should perform MAM enrollment check when skipMAMEnrollmentCheck is false', async () => {
const ensureMAMSpy = jest.spyOn(SecurityManager, 'ensureMAMEnrollmentForActiveServer').mockResolvedValue(true);
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: false}});
expect(ensureMAMSpy).toHaveBeenCalledWith(serverUrl1);
ensureMAMSpy.mockRestore();
});
test('should skip jailbreak check when skipJailbreakCheck is true', async () => {
const jailbreakSpy = jest.spyOn(SecurityManager, 'isDeviceJailbroken');
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: true, skipJailbreakCheck: true}});
expect(jailbreakSpy).not.toHaveBeenCalled();
jailbreakSpy.mockRestore();
});
test('should perform jailbreak check when skipJailbreakCheck is false', async () => {
const jailbreakSpy = jest.spyOn(SecurityManager, 'isDeviceJailbroken').mockResolvedValue(false);
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: true, skipJailbreakCheck: false}});
expect(jailbreakSpy).toHaveBeenCalledWith(serverUrl1);
jailbreakSpy.mockRestore();
});
test('should skip biometric auth when skipBiometricCheck is true', async () => {
const biometricSpy = jest.spyOn(SecurityManager, 'authenticateWithBiometricsIfNeeded');
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: true, skipJailbreakCheck: true, skipBiometricCheck: true}});
expect(biometricSpy).not.toHaveBeenCalled();
biometricSpy.mockRestore();
});
test('should perform biometric auth when skipBiometricCheck is false', async () => {
const biometricSpy = jest.spyOn(SecurityManager, 'authenticateWithBiometricsIfNeeded');
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: true, skipJailbreakCheck: true, skipBiometricCheck: false}});
expect(biometricSpy).toHaveBeenCalledWith(serverUrl1);
biometricSpy.mockRestore();
});
test('should force switch to same server when forceSwitch is true', async () => {
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: true, skipJailbreakCheck: true, skipBiometricCheck: true}});
const setScreenCaptureSpy = jest.spyOn(SecurityManager, 'setScreenCapturePolicy');
setScreenCaptureSpy.mockClear();
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: true, skipJailbreakCheck: true, skipBiometricCheck: true, forceSwitch: true}});
expect(setScreenCaptureSpy).toHaveBeenCalledWith(serverUrl1);
setScreenCaptureSpy.mockRestore();
});
test('should not switch to same server when forceSwitch is false', async () => {
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: true, skipJailbreakCheck: true, skipBiometricCheck: true}});
const setScreenCaptureSpy = jest.spyOn(SecurityManager, 'setScreenCapturePolicy');
setScreenCaptureSpy.mockClear();
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: true, skipJailbreakCheck: true, skipBiometricCheck: true, forceSwitch: false}});
expect(setScreenCaptureSpy).not.toHaveBeenCalled();
setScreenCaptureSpy.mockRestore();
});
});
});
});