88 * of the MIT license. See the LICENSE file for details.
99 */
1010
11- import { WebAuthnStepType } from './enums' ;
11+ import { WebAuthnOutcome , WebAuthnStepType } from './enums' ;
1212import FRWebAuthn from './index' ;
1313import {
1414 webAuthnRegJSCallback653 ,
@@ -23,6 +23,7 @@ import {
2323 webAuthnAuthMetaCallback70StoredUsername ,
2424 webAuthnAuthConditionalMetaCallback ,
2525} from './fr-webauthn.mock.data' ;
26+ import { CallbackType } from '../auth/enums' ;
2627import FRStep from '../fr-auth/fr-step' ;
2728import Config from '../config' ;
2829
@@ -245,3 +246,62 @@ describe('Test FRWebAuthn class with Conditional UI', () => {
245246 expect ( Array . from ( idArray ) ) . toEqual ( [ 1 , 2 , 3 , 4 ] ) ;
246247 } ) ;
247248} ) ;
249+
250+ describe ( 'Test FRWebAuthn class with cancellation error handling' , ( ) => {
251+ beforeEach ( ( ) => {
252+ Object . defineProperty ( global . navigator , 'credentials' , {
253+ value : {
254+ get : vi . fn ( ) ,
255+ create : vi . fn ( ) ,
256+ } ,
257+ writable : true ,
258+ } ) ;
259+ Object . defineProperty ( window , 'PublicKeyCredential' , {
260+ value : {
261+ // Mocked as supported so conditional mediation checks pass through to the credential call
262+ isConditionalMediationAvailable : vi . fn ( ) . mockResolvedValue ( true ) ,
263+ } ,
264+ writable : true ,
265+ } ) ;
266+ } ) ;
267+
268+ afterEach ( ( ) => {
269+ vi . restoreAllMocks ( ) ;
270+ } ) ;
271+
272+ it ( 'should write NotAllowedError to HiddenValueCallback when user cancels conditional authentication' , async ( ) => {
273+ const cancelError = new Error ( 'The operation either timed out or was not allowed.' ) ;
274+ cancelError . name = 'NotAllowedError' ;
275+ vi . spyOn ( navigator . credentials , 'get' ) . mockRejectedValue ( cancelError ) ;
276+
277+ const step = new FRStep ( webAuthnAuthConditionalMetaCallback as any ) ;
278+
279+ await expect ( FRWebAuthn . authenticate ( step ) ) . rejects . toMatchObject ( {
280+ name : 'NotAllowedError' ,
281+ } ) ;
282+
283+ const hiddenCallback = step . getCallbacksOfType ( CallbackType . HiddenValueCallback ) [ 0 ] ;
284+ expect ( hiddenCallback ) . toBeDefined ( ) ;
285+ expect ( hiddenCallback . getInputValue ( ) ) . toBe (
286+ `${ WebAuthnOutcome . Error } ::NotAllowedError:The operation either timed out or was not allowed.` ,
287+ ) ;
288+ } ) ;
289+
290+ it ( 'should write NotAllowedError to HiddenValueCallback when user cancels standard authentication' , async ( ) => {
291+ const cancelError = new Error ( 'The operation either timed out or was not allowed.' ) ;
292+ cancelError . name = 'NotAllowedError' ;
293+ vi . spyOn ( navigator . credentials , 'get' ) . mockRejectedValue ( cancelError ) ;
294+
295+ const step = new FRStep ( webAuthnAuthMetaCallback70 as any ) ;
296+
297+ await expect ( FRWebAuthn . authenticate ( step ) ) . rejects . toMatchObject ( {
298+ name : 'NotAllowedError' ,
299+ } ) ;
300+
301+ const hiddenCallback = step . getCallbacksOfType ( CallbackType . HiddenValueCallback ) [ 0 ] ;
302+ expect ( hiddenCallback ) . toBeDefined ( ) ;
303+ expect ( hiddenCallback . getInputValue ( ) ) . toBe (
304+ `${ WebAuthnOutcome . Error } ::NotAllowedError:The operation either timed out or was not allowed.` ,
305+ ) ;
306+ } ) ;
307+ } ) ;
0 commit comments