Skip to content

Commit 438a606

Browse files
committed
refactor(mfa): unify the controlled actor mocks behind a factory
1 parent 1c4b082 commit 438a606

2 files changed

Lines changed: 42 additions & 74 deletions

File tree

tests/unit/components/MultifactorAuthentication/machine/graphTraversal/viewMatchesMachine.test.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import getWalkedPaths, {
2222
} from 'tests/utils/mfa/flowPaths';
2323
import {getSettleableLeafStates} from 'tests/utils/mfa/leafStates';
2424
import renderMfaUi from 'tests/utils/mfa/realUi/harness';
25-
import {pendingModalClose, rejectSoftPromptAcceptanceRead, rejectValidateDevice, resetMfaUiMocks, resolveSoftPromptAcceptance, resolveValidateDevice} from 'tests/utils/mfa/realUi/mocks';
25+
import {pendingModalClose, readHasAcceptedSoftPromptControl, resetMfaUiMocks, validateDeviceControl} from 'tests/utils/mfa/realUi/mocks';
2626
import {translateLocal} from 'tests/utils/TestHelper';
2727
import waitForBatchedUpdatesWithAct from 'tests/utils/waitForBatchedUpdatesWithAct';
2828
import {matchesState} from 'xstate';
@@ -122,10 +122,10 @@ function createMfaEventExecutors(executeScenario: ExecuteScenario) {
122122
fireEvent.press(screen.getByTestId(TEST_ID.PROMPT_CONFIRM_BUTTON));
123123
await waitForBatchedUpdatesWithAct();
124124
},
125-
[VALIDATE_DEVICE_DONE_EVENT_TYPE]: (step) => settleActor(() => resolveValidateDevice(step.event.output)),
126-
[VALIDATE_DEVICE_ERROR_EVENT_TYPE]: () => settleActor(rejectValidateDevice),
127-
[READ_HAS_ACCEPTED_SOFT_PROMPT_DONE_EVENT_TYPE]: (step) => settleActor(() => resolveSoftPromptAcceptance(step.event.output)),
128-
[READ_HAS_ACCEPTED_SOFT_PROMPT_ERROR_EVENT_TYPE]: () => settleActor(rejectSoftPromptAcceptanceRead),
125+
[VALIDATE_DEVICE_DONE_EVENT_TYPE]: (step) => settleActor(() => validateDeviceControl.resolve(step.event.output)),
126+
[VALIDATE_DEVICE_ERROR_EVENT_TYPE]: () => settleActor(validateDeviceControl.reject),
127+
[READ_HAS_ACCEPTED_SOFT_PROMPT_DONE_EVENT_TYPE]: (step) => settleActor(() => readHasAcceptedSoftPromptControl.resolve(step.event.output)),
128+
[READ_HAS_ACCEPTED_SOFT_PROMPT_ERROR_EVENT_TYPE]: () => settleActor(readHasAcceptedSoftPromptControl.reject),
129129
} satisfies MfaEventExecutors & MfaActorEventExecutors;
130130
}
131131
/* eslint-enable @typescript-eslint/naming-convention */

tests/utils/mfa/realUi/mocks.ts

Lines changed: 37 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,8 @@ import {fromPromise} from 'xstate';
1212

1313
type CapturedCallback = () => void;
1414
type NavigationTransitionOverrides = Pick<typeof Navigation, 'runAfterTransition' | 'runAfterUpcomingTransition'>;
15-
type PendingValidateDevice = {
16-
resolve: (result: MFAResult) => void;
17-
reject: (error: Error) => void;
18-
};
19-
type PendingReadHasAcceptedSoftPrompt = {
20-
resolve: (accepted: boolean) => void;
15+
type PendingCall<TOutput> = {
16+
resolve: (output: TOutput) => void;
2117
reject: (error: Error) => void;
2218
};
2319

@@ -54,68 +50,52 @@ const biometricsMock: Pick<UseBiometricsReturn, 'serverKnownCredentialIDs' | 'ar
5450
areLocalCredentialsKnownToServer: () => Promise.resolve(false),
5551
};
5652

57-
let pendingValidateDeviceCall: PendingValidateDevice | undefined;
58-
let pendingSoftPromptAcceptanceCall: PendingReadHasAcceptedSoftPrompt | undefined;
59-
60-
function takePendingValidateDeviceCall(): PendingValidateDevice {
61-
const pendingCall = pendingValidateDeviceCall;
62-
pendingValidateDeviceCall = undefined;
63-
if (!pendingCall) {
64-
throw new Error('No pending validateDevice call is available.');
65-
}
66-
return pendingCall;
67-
}
68-
69-
function resolveValidateDevice(result: MFAResult) {
70-
takePendingValidateDeviceCall().resolve(result);
71-
}
72-
73-
function rejectValidateDevice() {
74-
takePendingValidateDeviceCall().reject(new Error('Mock validateDevice actor rejected for this path'));
75-
}
76-
77-
function takePendingSoftPromptAcceptanceCall(): PendingReadHasAcceptedSoftPrompt {
78-
const pendingCall = pendingSoftPromptAcceptanceCall;
79-
pendingSoftPromptAcceptanceCall = undefined;
80-
if (!pendingCall) {
81-
throw new Error('No pending readHasAcceptedSoftPrompt call is available.');
53+
/**
54+
* Builds a controlled deferred mock for one invoked machine actor. Each machine invocation parks a
55+
* pending promise that the test settles later through `resolve` or `reject`, at the exact path step
56+
* where the machine expects the actor outcome.
57+
*/
58+
function createControlledActor<TOutput, TInput>(actorID: string) {
59+
let pendingCall: PendingCall<TOutput> | undefined;
60+
61+
function takePendingCall(): PendingCall<TOutput> {
62+
const call = pendingCall;
63+
pendingCall = undefined;
64+
if (!call) {
65+
throw new Error(`No pending ${actorID} call is available.`);
66+
}
67+
return call;
8268
}
83-
return pendingCall;
84-
}
8569

86-
function resolveSoftPromptAcceptance(accepted: boolean) {
87-
takePendingSoftPromptAcceptanceCall().resolve(accepted);
70+
return {
71+
actor: fromPromise<TOutput, TInput>(
72+
() =>
73+
new Promise<TOutput>((resolve, reject) => {
74+
pendingCall = {resolve, reject};
75+
}),
76+
),
77+
resolve: (output: TOutput) => takePendingCall().resolve(output),
78+
reject: () => takePendingCall().reject(new Error(`Mock ${actorID} actor rejected for this path`)),
79+
reset: () => {
80+
pendingCall = undefined;
81+
},
82+
};
8883
}
8984

90-
function rejectSoftPromptAcceptanceRead() {
91-
takePendingSoftPromptAcceptanceCall().reject(new Error('Mock readHasAcceptedSoftPrompt actor rejected for this path'));
92-
}
85+
const validateDeviceControl = createControlledActor<MFAResult, ValidateDeviceInput>('validateDevice');
86+
const readHasAcceptedSoftPromptControl = createControlledActor<boolean, ReadHasAcceptedSoftPromptInput>('readHasAcceptedSoftPrompt');
9387

9488
function resetMfaUiMocks() {
9589
pendingModalClose.clear();
96-
pendingValidateDeviceCall = undefined;
97-
pendingSoftPromptAcceptanceCall = undefined;
90+
validateDeviceControl.reset();
91+
readHasAcceptedSoftPromptControl.reset();
9892
}
9993

100-
const validateDeviceMock = fromPromise<MFAResult, ValidateDeviceInput>(
101-
() =>
102-
new Promise<MFAResult>((resolve, reject) => {
103-
pendingValidateDeviceCall = {resolve, reject};
104-
}),
105-
);
106-
107-
const readHasAcceptedSoftPromptMock = fromPromise<boolean, ReadHasAcceptedSoftPromptInput>(
108-
() =>
109-
new Promise<boolean>((resolve, reject) => {
110-
pendingSoftPromptAcceptanceCall = {resolve, reject};
111-
}),
112-
);
113-
11494
/** Replaces the machine's side-effect actors with controlled test implementations. */
11595
function mfaActorsMock() {
11696
const actors = {
117-
validateDevice: validateDeviceMock,
118-
readHasAcceptedSoftPrompt: readHasAcceptedSoftPromptMock,
97+
validateDevice: validateDeviceControl.actor,
98+
readHasAcceptedSoftPrompt: readHasAcceptedSoftPromptControl.actor,
11999
} satisfies ReturnType<typeof createActors>;
120100

121101
return {
@@ -173,16 +153,4 @@ function navigationMock() {
173153
};
174154
}
175155

176-
export {
177-
pendingModalClose,
178-
resolveValidateDevice,
179-
rejectValidateDevice,
180-
resolveSoftPromptAcceptance,
181-
rejectSoftPromptAcceptanceRead,
182-
resetMfaUiMocks,
183-
mfaActorsMock,
184-
biometricsHookMock,
185-
renderHtmlMock,
186-
syncHistoryMock,
187-
navigationMock,
188-
};
156+
export {pendingModalClose, validateDeviceControl, readHasAcceptedSoftPromptControl, resetMfaUiMocks, mfaActorsMock, biometricsHookMock, renderHtmlMock, syncHistoryMock, navigationMock};

0 commit comments

Comments
 (0)