mirror of
https://github.com/mattermost/mattermost-mobile.git
synced 2026-11-05 02:18:56 +00:00
* On iOS, resuming after the 5-minute window showed no biometric prompt — just a blocking alert whose only button logged the user out. A failure to evaluate device security was indistinguishable from a verdict that the device is not secured. Android had the same class of bug, where user-cancel codes led to logout. Outcomes are now classified three ways, so only a genuine credential rejection or a genuinely unsecured device acts destructively; cancels and indeterminate results offer a retry. SecurityManager and ManagedApp share the policy-free parts via app/utils/security/. Also fixes a resume stacking a second prompt behind an open alert, a backgroundSince bypass, and a latched flag that disabled the MDM PIN. * feedback review * gate app state * fix tests
89 lines
3.5 KiB
TypeScript
89 lines
3.5 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import Emm, {AuthenticationOutcome} from '@mattermost/react-native-emm';
|
|
|
|
import {advanceTimers, disableFakeTimers, enableFakeTimers} from '@test/timer_helpers';
|
|
|
|
import {getAuthenticationOutcome, probeDeviceSecured} from './authentication';
|
|
import {AuthenticationSource, DEVICE_SECURED_RETRY_DELAY} from './constants';
|
|
|
|
jest.mock('@mattermost/react-native-emm', () => ({
|
|
AuthenticationOutcome: {
|
|
Failed: 'E_AUTH_FAILED',
|
|
Cancelled: 'E_CANCELLED',
|
|
Indeterminate: 'E_INDETERMINATE',
|
|
},
|
|
isDeviceSecured: jest.fn(),
|
|
}));
|
|
|
|
const rejectWith = (outcome: AuthenticationOutcome) => {
|
|
const error = new Error(outcome) as Error & {outcome: AuthenticationOutcome};
|
|
error.outcome = outcome;
|
|
return error;
|
|
};
|
|
|
|
describe('getAuthenticationOutcome', () => {
|
|
test('should return the outcome carried by the error', () => {
|
|
expect(getAuthenticationOutcome(rejectWith(AuthenticationOutcome.Failed))).toBe(AuthenticationOutcome.Failed);
|
|
expect(getAuthenticationOutcome(rejectWith(AuthenticationOutcome.Cancelled))).toBe(AuthenticationOutcome.Cancelled);
|
|
});
|
|
|
|
test('should treat an unrecognised error as indeterminate so it never looks like a failure', () => {
|
|
expect(getAuthenticationOutcome(new Error('boom'))).toBe(AuthenticationOutcome.Indeterminate);
|
|
expect(getAuthenticationOutcome(undefined)).toBe(AuthenticationOutcome.Indeterminate);
|
|
});
|
|
});
|
|
|
|
describe('probeDeviceSecured', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
enableFakeTimers();
|
|
});
|
|
|
|
afterEach(() => {
|
|
disableFakeTimers();
|
|
});
|
|
|
|
test('should report secured without retrying', async () => {
|
|
jest.mocked(Emm.isDeviceSecured).mockResolvedValue(true);
|
|
|
|
await expect(probeDeviceSecured(AuthenticationSource.SecurityManager)).resolves.toBe('secured');
|
|
expect(Emm.isDeviceSecured).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
test('should not retry a device that is genuinely not secured', async () => {
|
|
jest.mocked(Emm.isDeviceSecured).mockResolvedValue(false);
|
|
|
|
await expect(probeDeviceSecured(AuthenticationSource.SecurityManager)).resolves.toBe('notSecured');
|
|
expect(Emm.isDeviceSecured).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
test('should retry once and succeed when the first check cannot be performed', async () => {
|
|
jest.mocked(Emm.isDeviceSecured).
|
|
mockRejectedValueOnce(rejectWith(AuthenticationOutcome.Indeterminate)).
|
|
mockResolvedValueOnce(true);
|
|
|
|
const result = probeDeviceSecured(AuthenticationSource.SecurityManager);
|
|
|
|
// Let the first rejection settle so the retry delay is actually pending.
|
|
await new Promise(process.nextTick);
|
|
await advanceTimers(DEVICE_SECURED_RETRY_DELAY);
|
|
|
|
await expect(result).resolves.toBe('secured');
|
|
expect(Emm.isDeviceSecured).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
test('should report interrupted rather than notSecured when both attempts fail', async () => {
|
|
jest.mocked(Emm.isDeviceSecured).mockRejectedValue(rejectWith(AuthenticationOutcome.Indeterminate));
|
|
|
|
const result = probeDeviceSecured(AuthenticationSource.SecurityManager);
|
|
|
|
await new Promise(process.nextTick);
|
|
await advanceTimers(DEVICE_SECURED_RETRY_DELAY);
|
|
|
|
await expect(result).resolves.toBe('interrupted');
|
|
expect(Emm.isDeviceSecured).toHaveBeenCalledTimes(2);
|
|
});
|
|
});
|