Skip to content

Commit 0113a3d

Browse files
committed
test(mfa): extract the shared actor setup into a flow-actors util
1 parent 8d11d2c commit 0113a3d

3 files changed

Lines changed: 59 additions & 68 deletions

File tree

tests/unit/components/MultifactorAuthentication/machine/outcomeGuard.test.ts

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,18 @@
1-
import mfaMachine from '@components/MultifactorAuthentication/machine/mfaMachine';
2-
import type {MfaEvent} from '@components/MultifactorAuthentication/machine/types';
3-
41
import {createLocalMFAError} from '@libs/MultifactorAuthentication/shared/MFAResult';
52

63
import CONST from '@src/CONST';
74

8-
import createInitEvent from 'tests/utils/mfa/flowFixtures';
9-
import {VALIDATE_DEVICE_DONE_EVENT_TYPE} from 'tests/utils/mfa/flowPaths';
10-
import {createActor} from 'xstate';
5+
import {createActorAtState, sendValidateDeviceDone} from 'tests/utils/mfa/flowActors';
116

127
const MFA_STATE = CONST.MULTIFACTOR_AUTHENTICATION.MFA_STATE;
138

149
describe('MFA outcome guard', () => {
1510
it('routes a successful actor result to failure when the context already contains an error', () => {
16-
const initEvent = createInitEvent();
1711
const existingError = createLocalMFAError(CONST.MULTIFACTOR_AUTHENTICATION.REASON.LOCAL_ERRORS.UNHANDLED_EXCEPTION, 'Existing flow error');
18-
const validatingDeviceSnapshot = mfaMachine.resolveState({
19-
value: {[MFA_STATE.OPEN]: {[MFA_STATE.PREPARING]: MFA_STATE.VALIDATING_DEVICE}},
20-
context: {
21-
error: existingError,
22-
scenarioName: initEvent.scenarioName,
23-
scenario: initEvent.scenario,
24-
payload: initEvent.payload,
25-
softPromptApproved: false,
26-
isCancelConfirmVisible: false,
27-
},
28-
});
29-
const actor = createActor(mfaMachine, {snapshot: validatingDeviceSnapshot});
12+
const actor = createActorAtState({[MFA_STATE.OPEN]: {[MFA_STATE.PREPARING]: MFA_STATE.VALIDATING_DEVICE}}, {error: existingError});
3013

3114
actor.start();
32-
// Framework actor events are not part of the application's MfaEvent union.
33-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
34-
actor.send({type: VALIDATE_DEVICE_DONE_EVENT_TYPE, output: {success: true}} as unknown as MfaEvent);
15+
sendValidateDeviceDone(actor, {success: true});
3516

3617
const result = actor.getSnapshot();
3718
expect(result.matches({[MFA_STATE.OPEN]: {[MFA_STATE.OUTCOME]: MFA_STATE.FAILURE}})).toBe(true);

tests/unit/components/MultifactorAuthentication/machine/softPromptTransition.test.ts

Lines changed: 7 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,12 @@
1-
import mfaMachine from '@components/MultifactorAuthentication/machine/mfaMachine';
2-
import type {MfaContext, MfaEvent} from '@components/MultifactorAuthentication/machine/types';
3-
41
import {getDeviceBiometricsOnyxKey} from '@libs/actions/MultifactorAuthentication';
52

63
import CONST from '@src/CONST';
74
import ONYXKEYS from '@src/ONYXKEYS';
85

96
import Onyx from 'react-native-onyx';
107
import getOnyxValue from 'tests/utils/getOnyxValue';
11-
import createInitEvent from 'tests/utils/mfa/flowFixtures';
12-
import {VALIDATE_DEVICE_DONE_EVENT_TYPE} from 'tests/utils/mfa/flowPaths';
8+
import {createActorAtState, sendValidateDeviceDone} from 'tests/utils/mfa/flowActors';
139
import waitForBatchedUpdates from 'tests/utils/waitForBatchedUpdates';
14-
import {createActor} from 'xstate';
1510

1611
const MFA_STATE = CONST.MULTIFACTOR_AUTHENTICATION.MFA_STATE;
1712
const TEST_ACCOUNT_ID = 12345;
@@ -20,51 +15,17 @@ const TEST_ACCOUNT_ID = 12345;
2015
// a wrong target adjusts those expectations and still passes. This suite pins the soft-prompt hops by
2116
// hand: eligible device -> soft prompt, and approval -> success outcome plus the persisted acceptance.
2217

23-
function createCleanContext(): MfaContext {
24-
const initEvent = createInitEvent();
25-
return {
26-
error: undefined,
27-
scenarioName: initEvent.scenarioName,
28-
scenario: initEvent.scenario,
29-
payload: initEvent.payload,
30-
softPromptApproved: false,
31-
isCancelConfirmVisible: false,
32-
};
33-
}
34-
35-
function createAwaitingSoftPromptActor() {
36-
const awaitingSoftPromptSnapshot = mfaMachine.resolveState({
37-
value: {[MFA_STATE.OPEN]: {[MFA_STATE.PROMPT]: MFA_STATE.AWAITING_SOFT_PROMPT}},
38-
context: createCleanContext(),
39-
});
40-
return createActor(mfaMachine, {snapshot: awaitingSoftPromptSnapshot});
41-
}
42-
43-
function createValidatingDeviceActor() {
44-
const validatingDeviceSnapshot = mfaMachine.resolveState({
45-
value: {[MFA_STATE.OPEN]: {[MFA_STATE.PREPARING]: MFA_STATE.VALIDATING_DEVICE}},
46-
context: createCleanContext(),
47-
});
48-
return createActor(mfaMachine, {snapshot: validatingDeviceSnapshot});
49-
}
50-
51-
function settleEligibleDevice(actor: ReturnType<typeof createValidatingDeviceActor>) {
52-
// Framework actor events are not part of the application's MfaEvent union.
53-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
54-
actor.send({type: VALIDATE_DEVICE_DONE_EVENT_TYPE, output: {success: true}} as unknown as MfaEvent);
55-
}
56-
5718
describe('MFA soft prompt', () => {
5819
afterEach(async () => {
5920
await Onyx.clear();
6021
await waitForBatchedUpdates();
6122
});
6223

6324
it('moves an eligible device to the soft prompt without approving it', () => {
64-
const actor = createValidatingDeviceActor();
25+
const actor = createActorAtState({[MFA_STATE.OPEN]: {[MFA_STATE.PREPARING]: MFA_STATE.VALIDATING_DEVICE}});
6526

6627
actor.start();
67-
settleEligibleDevice(actor);
28+
sendValidateDeviceDone(actor, {success: true});
6829

6930
const result = actor.getSnapshot();
7031
expect(result.matches({[MFA_STATE.OPEN]: {[MFA_STATE.PROMPT]: MFA_STATE.AWAITING_SOFT_PROMPT}})).toBe(true);
@@ -79,10 +40,10 @@ describe('MFA soft prompt', () => {
7940
await Onyx.merge(ONYXKEYS.SESSION, {accountID: TEST_ACCOUNT_ID});
8041
await Onyx.merge(getDeviceBiometricsOnyxKey(TEST_ACCOUNT_ID), {hasAcceptedSoftPrompt: true});
8142
await waitForBatchedUpdates();
82-
const actor = createValidatingDeviceActor();
43+
const actor = createActorAtState({[MFA_STATE.OPEN]: {[MFA_STATE.PREPARING]: MFA_STATE.VALIDATING_DEVICE}});
8344

8445
actor.start();
85-
settleEligibleDevice(actor);
46+
sendValidateDeviceDone(actor, {success: true});
8647

8748
const result = actor.getSnapshot();
8849
expect(result.matches({[MFA_STATE.OPEN]: {[MFA_STATE.OUTCOME]: MFA_STATE.SUCCESS}})).toBe(true);
@@ -93,7 +54,7 @@ describe('MFA soft prompt', () => {
9354
});
9455

9556
it('reaches the success outcome when the user approves the soft prompt', () => {
96-
const actor = createAwaitingSoftPromptActor();
57+
const actor = createActorAtState({[MFA_STATE.OPEN]: {[MFA_STATE.PROMPT]: MFA_STATE.AWAITING_SOFT_PROMPT}});
9758

9859
actor.start();
9960
actor.send({type: 'SOFT_PROMPT_APPROVED'});
@@ -110,7 +71,7 @@ describe('MFA soft prompt', () => {
11071
// the session must settle before the event fires.
11172
await Onyx.merge(ONYXKEYS.SESSION, {accountID: TEST_ACCOUNT_ID});
11273
await waitForBatchedUpdates();
113-
const actor = createAwaitingSoftPromptActor();
74+
const actor = createActorAtState({[MFA_STATE.OPEN]: {[MFA_STATE.PROMPT]: MFA_STATE.AWAITING_SOFT_PROMPT}});
11475

11576
actor.start();
11677
actor.send({type: 'SOFT_PROMPT_APPROVED'});

tests/utils/mfa/flowActors.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import type createActors from '@components/MultifactorAuthentication/machine/mfaActors';
2+
import mfaMachine from '@components/MultifactorAuthentication/machine/mfaMachine';
3+
import type {MfaContext, MfaEvent} from '@components/MultifactorAuthentication/machine/types';
4+
5+
import type {OutputFrom, StateValue} from 'xstate';
6+
7+
import {createActor} from 'xstate';
8+
9+
import createInitEvent from './flowFixtures';
10+
import {VALIDATE_DEVICE_DONE_EVENT_TYPE} from './flowPaths';
11+
12+
type ValidateDeviceOutput = OutputFrom<ReturnType<typeof createActors>['validateDevice']>;
13+
14+
/**
15+
* Builds the context a flow carries right after INIT seeds it. Overrides express a spec's starting
16+
* variation, such as a stored error.
17+
*/
18+
function createFlowContext(overrides: Partial<MfaContext> = {}): MfaContext {
19+
const initEvent = createInitEvent();
20+
return {
21+
error: undefined,
22+
scenarioName: initEvent.scenarioName,
23+
scenario: initEvent.scenario,
24+
payload: initEvent.payload,
25+
softPromptApproved: false,
26+
isCancelConfirmVisible: false,
27+
...overrides,
28+
};
29+
}
30+
31+
/**
32+
* Creates an actor resolved to the given state value over a fresh post-INIT context, so a transition
33+
* spec can drive a single hop without walking the whole flow first. The actor is not started.
34+
*/
35+
function createActorAtState(value: StateValue, contextOverrides?: Partial<MfaContext>) {
36+
const snapshot = mfaMachine.resolveState({value, context: createFlowContext(contextOverrides)});
37+
return createActor(mfaMachine, {snapshot});
38+
}
39+
40+
/**
41+
* Completes the invoked device-check actor by sending its done event carrying the given output.
42+
*/
43+
function sendValidateDeviceDone(actor: ReturnType<typeof createActorAtState>, output: ValidateDeviceOutput) {
44+
// Framework actor events are not part of the application's MfaEvent union.
45+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
46+
actor.send({type: VALIDATE_DEVICE_DONE_EVENT_TYPE, output} as unknown as MfaEvent);
47+
}
48+
49+
export {createActorAtState, createFlowContext, sendValidateDeviceDone};

0 commit comments

Comments
 (0)