* 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>
659 lines
24 KiB
TypeScript
659 lines
24 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import {createIntl} from 'react-intl';
|
|
import {Alert, DeviceEventEmitter, Platform} from 'react-native';
|
|
|
|
import {cancelSessionNotification, findSession} from '@actions/local/session';
|
|
import {Events} from '@constants';
|
|
import {GLOBAL_IDENTIFIERS, SYSTEM_IDENTIFIERS} from '@constants/database';
|
|
import DatabaseManager from '@database/manager';
|
|
import NetworkManager from '@managers/network_manager';
|
|
import WebsocketManager from '@managers/websocket_manager';
|
|
import TestHelper from '@test/test_helper';
|
|
import {logWarning} from '@utils/log';
|
|
|
|
import {
|
|
addPushProxyVerificationStateFromLogin,
|
|
forceLogoutIfNecessary,
|
|
fetchSessions,
|
|
login,
|
|
logout,
|
|
nativeEntraLogin,
|
|
scheduleSessionNotification,
|
|
sendPasswordResetEmail,
|
|
ssoLogin,
|
|
} from './session';
|
|
|
|
import type ServerDataOperator from '@database/operator/server_data_operator';
|
|
import type {LoginArgs} from '@typings/database/database';
|
|
|
|
const intl = createIntl({
|
|
locale: 'en',
|
|
messages: {},
|
|
});
|
|
|
|
const serverUrl = 'baseHandler.test.com';
|
|
let operator: ServerDataOperator;
|
|
|
|
const user1 = TestHelper.fakeUser({id: 'userid1', username: 'user1', email: 'user1@mattermost.com', roles: ''});
|
|
|
|
const session1 = {id: 'sessionid1', user_id: user1.id, device_id: 'deviceid', props: {csrf: 'csrfid'}} as Session;
|
|
|
|
const throwFunc = () => {
|
|
throw Error('error');
|
|
};
|
|
|
|
const mockClient = {
|
|
login: jest.fn(() => user1),
|
|
loginByIntune: jest.fn().mockResolvedValue(user1),
|
|
setCSRFToken: jest.fn(),
|
|
setClientCredentials: jest.fn(),
|
|
getClientConfigOld: jest.fn(() => ({})),
|
|
getClientLicenseOld: jest.fn(() => ({})),
|
|
getSessions: jest.fn(() => [session1]),
|
|
sendPasswordResetEmail: jest.fn(() => ({status: 200})),
|
|
getMe: jest.fn(() => user1),
|
|
logout: jest.fn(),
|
|
};
|
|
|
|
const mockWebsocketClient = {
|
|
close: jest.fn(),
|
|
};
|
|
|
|
let mockGetPushProxyVerificationState: jest.Mock;
|
|
jest.mock('@store/ephemeral_store', () => {
|
|
const original = jest.requireActual('@store/ephemeral_store');
|
|
mockGetPushProxyVerificationState = jest.fn(() => 'verified');
|
|
return {
|
|
...original,
|
|
getPushProxyVerificationState: mockGetPushProxyVerificationState,
|
|
};
|
|
});
|
|
|
|
let mockFetch: jest.Mock;
|
|
jest.mock('@react-native-community/netinfo', () => {
|
|
const original = jest.requireActual('@react-native-community/netinfo');
|
|
mockFetch = jest.fn(() => ({isInternetReachable: true}));
|
|
return {
|
|
...original,
|
|
fetch: mockFetch,
|
|
};
|
|
});
|
|
|
|
let mockCancelLocalNotification: jest.Mock;
|
|
jest.mock('react-native-notifications', () => {
|
|
const original = jest.requireActual('react-native-notifications');
|
|
mockCancelLocalNotification = jest.fn();
|
|
return {
|
|
...original,
|
|
Notifications: {
|
|
...original.Notifications,
|
|
cancelLocalNotification: mockCancelLocalNotification,
|
|
},
|
|
};
|
|
});
|
|
|
|
let mockGetCSRFFromCookie: jest.Mock;
|
|
jest.mock('@utils/security', () => {
|
|
const original = jest.requireActual('@utils/security');
|
|
mockGetCSRFFromCookie = jest.fn(() => 'csrfid');
|
|
return {
|
|
...original,
|
|
getCSRFFromCookie: mockGetCSRFFromCookie,
|
|
};
|
|
});
|
|
|
|
jest.mock('@utils/log', () => {
|
|
const original = jest.requireActual('@utils/log');
|
|
return {
|
|
...original,
|
|
logWarning: jest.fn(),
|
|
};
|
|
});
|
|
|
|
beforeAll(() => {
|
|
// @ts-ignore
|
|
NetworkManager.getClient = () => mockClient;
|
|
|
|
// @ts-ignore
|
|
WebsocketManager.getClient = () => mockWebsocketClient;
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
await DatabaseManager.init([serverUrl]);
|
|
operator = DatabaseManager.serverDatabases[serverUrl]!.operator;
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await DatabaseManager.destroyServerDatabase(serverUrl);
|
|
});
|
|
|
|
describe('sessions', () => {
|
|
it('addPushProxyVerificationStateFromLogin - handle not found database', async () => {
|
|
const result = await addPushProxyVerificationStateFromLogin('foo');
|
|
expect(result).toBeDefined();
|
|
expect(result.error).toBeDefined();
|
|
});
|
|
|
|
it('addPushProxyVerificationStateFromLogin - no verification', async () => {
|
|
mockGetPushProxyVerificationState.mockImplementationOnce(() => '');
|
|
const result = await addPushProxyVerificationStateFromLogin(serverUrl);
|
|
expect(result).toBeDefined();
|
|
expect(result.error).toBeUndefined();
|
|
});
|
|
|
|
it('addPushProxyVerificationStateFromLogin - base case', async () => {
|
|
const result = await addPushProxyVerificationStateFromLogin(serverUrl);
|
|
expect(result).toBeDefined();
|
|
expect(result.error).toBeUndefined();
|
|
});
|
|
|
|
it('forceLogoutIfNecessary - handle not found database', async () => {
|
|
const result = await forceLogoutIfNecessary('foo', {});
|
|
expect(result).toBeDefined();
|
|
expect(result.error).toBeTruthy();
|
|
expect(result.logout).toBe(false);
|
|
});
|
|
|
|
it('forceLogoutIfNecessary - logout expected from 401', async () => {
|
|
await operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID, value: user1.id}], prepareRecordsOnly: false});
|
|
|
|
const result = await forceLogoutIfNecessary(serverUrl, {status_code: 401, url: '/api/v4/users/me'});
|
|
expect(result).toBeDefined();
|
|
expect(result.error).toBeNull();
|
|
expect(result.logout).toBe(true);
|
|
});
|
|
|
|
it('forceLogoutIfNecessary - logout not expected', async () => {
|
|
await operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID, value: user1.id}], prepareRecordsOnly: false});
|
|
|
|
const result = await forceLogoutIfNecessary(serverUrl, {status_code: 500, url: '/api/v4/users/me'});
|
|
expect(result).toBeDefined();
|
|
expect(result.error).toBeNull();
|
|
expect(result.logout).toBe(false);
|
|
});
|
|
|
|
it('fetchSessions - handle error', async () => {
|
|
mockClient.getSessions.mockImplementationOnce(jest.fn(throwFunc));
|
|
const result = await fetchSessions('foo', '');
|
|
expect(result).toBeUndefined();
|
|
});
|
|
|
|
it('fetchSessions - handle client error', async () => {
|
|
jest.spyOn(NetworkManager, 'getClient').mockImplementationOnce(throwFunc);
|
|
|
|
const result = await fetchSessions(serverUrl, user1.id);
|
|
expect(result).toBeUndefined();
|
|
});
|
|
|
|
it('fetchSessions - base case', async () => {
|
|
const result = await fetchSessions(serverUrl, user1.id);
|
|
expect(result).toBeDefined();
|
|
expect(result?.length).toBe(1);
|
|
});
|
|
|
|
it('login - base case', async () => {
|
|
const result = await login(serverUrl, {config: {DiagnosticId: 'diagnosticid'}} as LoginArgs);
|
|
expect(result).toBeDefined();
|
|
expect(result.error).toBeUndefined();
|
|
expect(result.failed).toBe(false);
|
|
});
|
|
|
|
it('login - handle throw on login request', async () => {
|
|
mockClient.login.mockImplementationOnce(jest.fn(throwFunc));
|
|
|
|
const result = await login(serverUrl, {config: {DiagnosticId: 'diagnosticid'}} as LoginArgs);
|
|
expect(result).toBeDefined();
|
|
expect(result.error).toBeDefined();
|
|
expect(result.failed).toBe(true);
|
|
});
|
|
|
|
it('login - handle throw after login request', async () => {
|
|
jest.spyOn(DatabaseManager, 'setActiveServerDatabase').mockImplementationOnce(throwFunc);
|
|
|
|
const result = await login(serverUrl, {config: {DiagnosticId: 'diagnosticid'}} as LoginArgs);
|
|
expect(result).toBeDefined();
|
|
expect(result.error).toBeDefined();
|
|
expect(result.failed).toBe(false);
|
|
});
|
|
|
|
it('cancelSessionNotification - handle not found database', async () => {
|
|
const result = await cancelSessionNotification('foo');
|
|
expect(result).toBeDefined();
|
|
expect(result.error).toBeDefined();
|
|
});
|
|
|
|
it('cancelSessionNotification - base case', async () => {
|
|
await operator.handleSystem({
|
|
systems: [{
|
|
id: SYSTEM_IDENTIFIERS.SESSION_EXPIRATION,
|
|
value: {
|
|
id: 'sessionid1',
|
|
notificationId: 'notificationid',
|
|
expiresAt: 123,
|
|
},
|
|
}],
|
|
prepareRecordsOnly: false,
|
|
});
|
|
|
|
const result = await cancelSessionNotification(serverUrl);
|
|
expect(result).toBeDefined();
|
|
expect(result.error).toBeUndefined();
|
|
});
|
|
|
|
it('cancelSessionNotification - no expired session', async () => {
|
|
const result = await cancelSessionNotification(serverUrl);
|
|
expect(result).toBeDefined();
|
|
expect(result.error).toBeUndefined();
|
|
});
|
|
|
|
it('scheduleSessionNotification - handle not found database', async () => {
|
|
const result = await scheduleSessionNotification('foo');
|
|
expect(result).toBeDefined();
|
|
expect(result.error).toBeDefined();
|
|
});
|
|
|
|
it('scheduleSessionNotification - base case', async () => {
|
|
await operator.handleSystem({
|
|
systems: [{
|
|
id: SYSTEM_IDENTIFIERS.SESSION_EXPIRATION,
|
|
value: {
|
|
id: 'sessionid1',
|
|
notificationId: 'notificationid',
|
|
expiresAt: 123,
|
|
},
|
|
}],
|
|
prepareRecordsOnly: false,
|
|
});
|
|
|
|
const result = await scheduleSessionNotification(serverUrl);
|
|
expect(result).toBeDefined();
|
|
expect(result.error).toBeUndefined();
|
|
});
|
|
|
|
it('scheduleSessionNotification - no session', async () => {
|
|
mockClient.getSessions.mockImplementationOnce(() => []);
|
|
const result = await scheduleSessionNotification(serverUrl);
|
|
expect(result).toBeDefined();
|
|
expect(result.error).toBeUndefined();
|
|
});
|
|
|
|
it('scheduleSessionNotification - null sessions', async () => {
|
|
mockClient.getSessions.mockImplementationOnce(() => null as any);
|
|
const result = await scheduleSessionNotification(serverUrl);
|
|
expect(result).toBeDefined();
|
|
expect(result.error).toBeUndefined();
|
|
});
|
|
|
|
it('sendPasswordResetEmail - handle error', async () => {
|
|
mockClient.sendPasswordResetEmail.mockImplementationOnce(jest.fn(throwFunc));
|
|
const result = await sendPasswordResetEmail('foo', '');
|
|
expect(result).toBeDefined();
|
|
expect(result.error).toBeDefined();
|
|
});
|
|
|
|
it('sendPasswordResetEmail - base case', async () => {
|
|
const result = await sendPasswordResetEmail(serverUrl, user1.email);
|
|
expect(result).toBeDefined();
|
|
expect(result.error).toBeUndefined();
|
|
expect(result.status).toBe(200);
|
|
});
|
|
|
|
it('ssoLogin - handle error', async () => {
|
|
mockClient.getMe.mockImplementationOnce(jest.fn(throwFunc));
|
|
const result = await ssoLogin('foo', '', '', '', '');
|
|
expect(result).toBeDefined();
|
|
expect(result.error).toBeDefined();
|
|
expect(result.failed).toBe(true);
|
|
});
|
|
|
|
it('ssoLogin - base case', async () => {
|
|
const result = await ssoLogin(serverUrl, 'servername', 'diagnosticid', 'authtoken', 'csrftoken');
|
|
expect(result).toBeDefined();
|
|
expect(result.error).toBeUndefined();
|
|
expect(result.failed).toBe(false);
|
|
});
|
|
|
|
it('ssoLogin - handle throw after login request', async () => {
|
|
jest.spyOn(DatabaseManager, 'setActiveServerDatabase').mockImplementationOnce(throwFunc);
|
|
|
|
const result = await ssoLogin(serverUrl, 'servername', 'diagnosticid', 'authtoken', 'csrftoken');
|
|
expect(result).toBeDefined();
|
|
expect(result.error).toBeDefined();
|
|
expect(result.failed).toBe(false);
|
|
});
|
|
|
|
it('findSession - handle not found database', async () => {
|
|
const result = await findSession('foo', []);
|
|
expect(result).toBeUndefined();
|
|
});
|
|
|
|
it('findSession - by id', async () => {
|
|
await operator.handleSystem({
|
|
systems: [{
|
|
id: SYSTEM_IDENTIFIERS.SESSION_EXPIRATION,
|
|
value: {
|
|
id: 'sessionid1',
|
|
notificationId: 'notificationid',
|
|
expiresAt: 123,
|
|
},
|
|
}],
|
|
prepareRecordsOnly: false,
|
|
});
|
|
|
|
const session = await findSession(serverUrl, [session1]);
|
|
expect(session).toBeDefined();
|
|
});
|
|
|
|
it('findSession - by device', async () => {
|
|
await DatabaseManager.appDatabase?.operator.handleGlobal({
|
|
globals: [{id: GLOBAL_IDENTIFIERS.DEVICE_TOKEN, value: 'deviceid'}],
|
|
prepareRecordsOnly: false,
|
|
});
|
|
|
|
const session = await findSession(serverUrl, [session1]);
|
|
expect(session).toBeDefined();
|
|
});
|
|
|
|
it('findSession - non-match device token', async () => {
|
|
await DatabaseManager.appDatabase?.operator.handleGlobal({
|
|
globals: [{id: GLOBAL_IDENTIFIERS.DEVICE_TOKEN, value: 'diffdeviceid'}],
|
|
prepareRecordsOnly: false,
|
|
});
|
|
|
|
const session = await findSession(serverUrl, [session1]);
|
|
expect(session).toBeDefined();
|
|
});
|
|
|
|
it('findSession - by csrf', async () => {
|
|
const session = await findSession(serverUrl, [session1]);
|
|
expect(session).toBeDefined();
|
|
});
|
|
|
|
it('findSession - no csrf token', async () => {
|
|
mockGetCSRFFromCookie.mockResolvedValueOnce('');
|
|
const session = await findSession(serverUrl, [session1]);
|
|
expect(session).toBeUndefined();
|
|
});
|
|
|
|
it('findSession - by os', async () => {
|
|
const session = await findSession(serverUrl, [{...session1, props: {os: Platform.OS, csrf: 'diffcsrfid'}}]);
|
|
expect(session).toBeDefined();
|
|
});
|
|
|
|
it('findSession - handle error', async () => {
|
|
jest.spyOn(DatabaseManager, 'getServerDatabaseAndOperator').mockImplementationOnce(throwFunc);
|
|
const result = await findSession(serverUrl, []);
|
|
expect(result).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('logout', () => {
|
|
const mockEmit = jest.spyOn(DeviceEventEmitter, 'emit').mockImplementation(() => true);
|
|
const mockAlert = jest.spyOn(Alert, 'alert').mockImplementation(() => true);
|
|
|
|
type TestCase = {
|
|
options: {
|
|
skipServerLogout: boolean;
|
|
logoutOnAlert: boolean;
|
|
removeServer: boolean;
|
|
skipEvents: boolean;
|
|
};
|
|
withIntl: boolean;
|
|
clientReturnError: boolean;
|
|
}
|
|
|
|
const testCases: TestCase[] = [];
|
|
for (const skipServerLogout of [false, true]) {
|
|
for (const logoutOnAlert of [false, true]) {
|
|
for (const removeServer of [false, true]) {
|
|
for (const skipEvents of [false, true]) {
|
|
for (const withIntl of [false, true]) {
|
|
for (const clientReturnError of [false, true]) {
|
|
testCases.push({options: {
|
|
skipServerLogout,
|
|
logoutOnAlert,
|
|
removeServer,
|
|
skipEvents},
|
|
withIntl,
|
|
clientReturnError});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
test.each(testCases)('%j', async ({clientReturnError, options, withIntl}) => {
|
|
if (clientReturnError) {
|
|
mockClient.logout.mockImplementationOnce(() => {
|
|
throw new Error('logout error');
|
|
});
|
|
} else {
|
|
mockClient.logout = jest.fn(() => ({status: 'OK'}));
|
|
}
|
|
|
|
const mockFormatMessage = jest.spyOn(intl, 'formatMessage');
|
|
|
|
const shouldCallClient = !options.skipServerLogout;
|
|
const shouldLogWarning = shouldCallClient && clientReturnError;
|
|
const shouldShowAlert = shouldCallClient && clientReturnError;
|
|
const shouldEmit = !options.skipEvents;
|
|
const shouldEmitBeforeAlert = shouldEmit && (!shouldShowAlert || options.logoutOnAlert);
|
|
const shouldEmitAfterAlert = shouldEmit && !shouldEmitBeforeAlert;
|
|
const expectedResult = !(shouldShowAlert && !options.logoutOnAlert);
|
|
const shouldCloseWebsocket = options.skipServerLogout || !clientReturnError || options.logoutOnAlert;
|
|
|
|
const clientCalls = shouldCallClient ? 1 : 0;
|
|
const alertButtons = options.logoutOnAlert ? 1 : 2;
|
|
|
|
const result = await logout(serverUrl, withIntl ? intl : undefined, {...options});
|
|
|
|
expect(result.data).toBe(expectedResult);
|
|
expect(mockClient.logout).toHaveBeenCalledTimes(clientCalls);
|
|
if (shouldLogWarning) {
|
|
expect(logWarning).toHaveBeenCalled();
|
|
}
|
|
|
|
if (shouldEmitBeforeAlert) {
|
|
expect(mockEmit).toHaveBeenCalledWith(Events.SERVER_LOGOUT, {serverUrl, removeServer: options.removeServer});
|
|
} else {
|
|
expect(mockEmit).not.toHaveBeenCalled();
|
|
}
|
|
|
|
if (shouldCloseWebsocket) {
|
|
expect(mockWebsocketClient.close).toHaveBeenCalledWith(true);
|
|
} else {
|
|
expect(mockWebsocketClient.close).not.toHaveBeenCalled();
|
|
}
|
|
|
|
if (shouldShowAlert) {
|
|
if (withIntl) {
|
|
expect(mockFormatMessage).toHaveBeenCalled();
|
|
}
|
|
expect(mockAlert).toHaveBeenCalled();
|
|
expect(mockAlert.mock.calls[0][2]).toHaveLength(alertButtons);
|
|
if (alertButtons === 2) {
|
|
mockAlert.mock.calls[0][2]?.[0].onPress?.();
|
|
expect(mockClient.logout).toHaveBeenCalledTimes(clientCalls);
|
|
expect(mockEmit).toHaveBeenCalledTimes(0);
|
|
}
|
|
|
|
mockAlert.mock.calls[0][2]?.[alertButtons - 1].onPress?.(); // Last button should be confirm
|
|
expect(mockClient.logout).toHaveBeenCalledTimes(clientCalls);
|
|
if (shouldEmitAfterAlert) {
|
|
expect(mockEmit).toHaveBeenCalledWith(Events.SERVER_LOGOUT, {serverUrl, removeServer: options.removeServer});
|
|
} else {
|
|
expect(mockEmit).toHaveBeenCalledTimes(shouldEmitBeforeAlert ? 1 : 0);
|
|
}
|
|
} else {
|
|
expect(mockFormatMessage).not.toHaveBeenCalled();
|
|
expect(mockAlert).not.toHaveBeenCalled();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('nativeEntraLogin', () => {
|
|
const serverDisplayName = 'Test Server';
|
|
const serverIdentifier = 'test-server-id';
|
|
const intuneScope = 'api://test-scope/.default';
|
|
|
|
const mockTokens = {
|
|
accessToken: 'mock-access-token',
|
|
idToken: 'mock-id-token',
|
|
identity: {
|
|
upn: 'test@example.com',
|
|
tid: 'tenant-id',
|
|
oid: 'object-id',
|
|
},
|
|
};
|
|
|
|
let IntuneManager: any;
|
|
|
|
beforeEach(() => {
|
|
// Get the mocked IntuneManager
|
|
IntuneManager = require('@managers/intune_manager').default;
|
|
|
|
// Reset all mocks
|
|
jest.clearAllMocks();
|
|
|
|
// Set up default implementations
|
|
IntuneManager.login.mockResolvedValue(mockTokens);
|
|
IntuneManager.enrollServer.mockResolvedValue(undefined);
|
|
IntuneManager.isManagedServer.mockResolvedValue(false);
|
|
|
|
mockClient.loginByIntune.mockReset().mockImplementation(() => Promise.resolve(user1));
|
|
mockGetCSRFFromCookie.mockImplementation(() => Promise.resolve('csrfid'));
|
|
});
|
|
|
|
it('should successfully login on first try without enrollment', async () => {
|
|
IntuneManager.isManagedServer.mockResolvedValue(false);
|
|
|
|
const result = await nativeEntraLogin(serverUrl, serverDisplayName, serverIdentifier, intuneScope);
|
|
|
|
expect(result.failed).toBe(false);
|
|
expect(IntuneManager.login).toHaveBeenCalledWith(serverUrl, [intuneScope]);
|
|
expect(mockClient.loginByIntune).toHaveBeenCalledWith(mockTokens.accessToken, expect.any(String));
|
|
expect(mockClient.setCSRFToken).toHaveBeenCalledWith('csrfid');
|
|
expect(IntuneManager.enrollServer).toHaveBeenCalledWith(serverUrl, mockTokens.identity);
|
|
});
|
|
|
|
it('should handle 401 token expiration and retry with refreshed token', async () => {
|
|
const refreshedTokens = {
|
|
...mockTokens,
|
|
accessToken: 'refreshed-access-token',
|
|
};
|
|
|
|
mockClient.loginByIntune.mockRejectedValueOnce({status_code: 401} as never).mockResolvedValueOnce(user1);
|
|
IntuneManager.login.mockResolvedValueOnce(mockTokens).mockResolvedValueOnce(refreshedTokens);
|
|
|
|
const result = await nativeEntraLogin(serverUrl, serverDisplayName, serverIdentifier, intuneScope);
|
|
|
|
expect(result.failed).toBe(false);
|
|
expect(IntuneManager.login).toHaveBeenCalledTimes(2);
|
|
expect(mockClient.loginByIntune).toHaveBeenCalledTimes(2);
|
|
expect(mockClient.loginByIntune).toHaveBeenNthCalledWith(2, refreshedTokens.accessToken, expect.any(String));
|
|
});
|
|
|
|
it('should handle 412 MAM enrollment required', async () => {
|
|
const error = {status_code: 412};
|
|
mockClient.loginByIntune.mockRejectedValueOnce(error);
|
|
|
|
const result = await nativeEntraLogin(serverUrl, serverDisplayName, serverIdentifier, intuneScope);
|
|
|
|
expect(result.failed).toBe(true);
|
|
expect(result.error).toEqual(error);
|
|
});
|
|
|
|
it('should handle 400 LDAP user missing error', async () => {
|
|
const error = {
|
|
status_code: 400,
|
|
server_error_id: 'ent.intune.login.ldap_user_missing.app_error',
|
|
};
|
|
mockClient.loginByIntune.mockRejectedValueOnce(error);
|
|
|
|
const result = await nativeEntraLogin(serverUrl, serverDisplayName, serverIdentifier, intuneScope);
|
|
|
|
expect(result.failed).toBe(true);
|
|
expect(result.error).toEqual(error);
|
|
});
|
|
|
|
it('should handle 409 user deactivated error', async () => {
|
|
const error = {status_code: 409};
|
|
mockClient.loginByIntune.mockRejectedValueOnce(error);
|
|
|
|
const result = await nativeEntraLogin(serverUrl, serverDisplayName, serverIdentifier, intuneScope);
|
|
|
|
expect(result.failed).toBe(true);
|
|
expect(result.error).toEqual(error);
|
|
});
|
|
|
|
it('should handle 428 account creation blocked error', async () => {
|
|
const error = {
|
|
status_code: 428,
|
|
server_error_id: 'ent.intune.login.account_creation_blocked.app_error',
|
|
};
|
|
mockClient.loginByIntune.mockRejectedValueOnce(error);
|
|
|
|
const result = await nativeEntraLogin(serverUrl, serverDisplayName, serverIdentifier, intuneScope);
|
|
|
|
expect(result.failed).toBe(true);
|
|
expect(result.error).toEqual(error);
|
|
});
|
|
|
|
it('should handle MSAL login failure', async () => {
|
|
const msalError = {
|
|
domain: 'MSALErrorDomain',
|
|
code: -50005,
|
|
message: 'User canceled authentication',
|
|
};
|
|
IntuneManager.login.mockRejectedValueOnce(msalError);
|
|
|
|
const result = await nativeEntraLogin(serverUrl, serverDisplayName, serverIdentifier, intuneScope);
|
|
|
|
expect(result.failed).toBe(true);
|
|
expect(result.error).toEqual(msalError);
|
|
});
|
|
|
|
it('should enroll in MAM after successful login if not already managed', async () => {
|
|
IntuneManager.isManagedServer.mockResolvedValue(false);
|
|
|
|
const result = await nativeEntraLogin(serverUrl, serverDisplayName, serverIdentifier, intuneScope);
|
|
|
|
expect(result.failed).toBe(false);
|
|
expect(IntuneManager.isManagedServer).toHaveBeenCalledWith(serverUrl);
|
|
expect(IntuneManager.enrollServer).toHaveBeenCalledWith(serverUrl, mockTokens.identity);
|
|
});
|
|
|
|
it('should skip MAM enrollment if already managed', async () => {
|
|
IntuneManager.isManagedServer.mockResolvedValue(true);
|
|
|
|
const result = await nativeEntraLogin(serverUrl, serverDisplayName, serverIdentifier, intuneScope);
|
|
|
|
expect(result.failed).toBe(false);
|
|
expect(IntuneManager.isManagedServer).toHaveBeenCalledWith(serverUrl);
|
|
expect(IntuneManager.enrollServer).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should fail if post-login MAM enrollment fails', async () => {
|
|
const enrollmentError = new Error('Enrollment failed');
|
|
IntuneManager.isManagedServer.mockResolvedValue(false);
|
|
IntuneManager.enrollServer.mockRejectedValueOnce(enrollmentError);
|
|
|
|
const result = await nativeEntraLogin(serverUrl, serverDisplayName, serverIdentifier, intuneScope);
|
|
|
|
expect(result.failed).toBe(true);
|
|
expect(result.error).toEqual(enrollmentError);
|
|
});
|
|
|
|
it('should handle generic server errors', async () => {
|
|
const error = {status_code: 500, message: 'Internal server error'};
|
|
mockClient.loginByIntune.mockRejectedValueOnce(error);
|
|
|
|
const result = await nativeEntraLogin(serverUrl, serverDisplayName, serverIdentifier, intuneScope);
|
|
|
|
expect(result.failed).toBe(true);
|
|
expect(result.error).toEqual(error);
|
|
});
|
|
});
|