33 *
44 * fr-webauthn.test.ts
55 *
6- * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved.
6+ * Copyright (c) 2020 - 2026 Ping Identity Corporation. All rights reserved.
77 * This software may be modified and distributed under the terms
88 * of the MIT license. See the LICENSE file for details.
99 */
@@ -23,6 +23,7 @@ import {
2323 webAuthnAuthMetaCallback70StoredUsername ,
2424} from './webauthn.mock.data.js' ;
2525import { createJourneyStep } from '../step.utils.js' ;
26+ import { vi , afterEach , beforeEach , expect } from 'vitest' ;
2627
2728describe ( 'Test FRWebAuthn class with 6.5.3 "Passwordless"' , ( ) => {
2829 it ( 'should return Registration type with register text-output callbacks' , ( ) => {
@@ -98,3 +99,104 @@ describe('Test FRWebAuthn class with 7.0 "Usernameless"', () => {
9899 expect ( stepType ) . toBe ( WebAuthnStepType . Authentication ) ;
99100 } ) ;
100101} ) ;
102+
103+ describe ( 'WebAuthn conditional mediation' , ( ) => {
104+ const originalNavigatorCredentials = navigator . credentials ;
105+ const originalPublicKeyCredential = globalThis . PublicKeyCredential ;
106+
107+ beforeEach ( ( ) => {
108+ Object . defineProperty ( globalThis , 'PublicKeyCredential' , {
109+ configurable : true ,
110+ writable : true ,
111+ value : class PublicKeyCredential {
112+ static async isConditionalMediationAvailable ( ) : Promise < boolean > {
113+ return true ;
114+ }
115+ } ,
116+ } ) ;
117+
118+ Object . defineProperty ( navigator , 'credentials' , {
119+ configurable : true ,
120+ value : {
121+ get : vi . fn ( ) ,
122+ } ,
123+ } ) ;
124+ } ) ;
125+
126+ afterEach ( ( ) => {
127+ Object . defineProperty ( navigator , 'credentials' , {
128+ configurable : true ,
129+ value : originalNavigatorCredentials ,
130+ } ) ;
131+
132+ Object . defineProperty ( globalThis , 'PublicKeyCredential' , {
133+ configurable : true ,
134+ writable : true ,
135+ value : originalPublicKeyCredential ,
136+ } ) ;
137+
138+ vi . restoreAllMocks ( ) ;
139+ } ) ;
140+
141+ it ( 'requires an AbortSignal when mediation is conditional' , async ( ) => {
142+ // eslint-disable-next-line
143+ const step = createJourneyStep ( webAuthnAuthMetaCallback70 as any ) ;
144+ const hiddenCallback = WebAuthn . getOutcomeCallback ( step ) ;
145+ if ( ! hiddenCallback ) throw new Error ( 'Missing hidden callback for test' ) ;
146+
147+ await expect ( WebAuthn . authenticate ( step , 'conditional' ) ) . rejects . toThrow (
148+ 'AbortSignal is required for conditional mediation WebAuthn requests' ,
149+ ) ;
150+
151+ expect ( hiddenCallback . getInputValue ( ) ) . toContain (
152+ 'AbortSignal is required for conditional mediation WebAuthn requests' ,
153+ ) ;
154+ } ) ;
155+
156+ it ( 'throws NotSupportedError when conditional mediation is not supported by the browser' , async ( ) => {
157+ // eslint-disable-next-line
158+ const step = createJourneyStep ( webAuthnAuthMetaCallback70 as any ) ;
159+ const hiddenCallback = WebAuthn . getOutcomeCallback ( step ) ;
160+ if ( ! hiddenCallback ) throw new Error ( 'Missing hidden callback for test' ) ;
161+
162+ const conditionalSupportSpy = vi
163+ . spyOn ( WebAuthn , 'isConditionalMediationSupported' )
164+ . mockResolvedValue ( false ) ;
165+
166+ await expect (
167+ WebAuthn . authenticate ( step , 'conditional' , new AbortController ( ) . signal ) ,
168+ ) . rejects . toMatchObject ( { name : 'NotSupportedError' } ) ;
169+
170+ expect ( conditionalSupportSpy ) . toHaveBeenCalledTimes ( 1 ) ;
171+ expect ( hiddenCallback . getInputValue ( ) ) . toBe ( 'unsupported' ) ;
172+ expect ( navigator . credentials . get as unknown as ReturnType < typeof vi . fn > ) . not . toHaveBeenCalled ( ) ;
173+ } ) ;
174+
175+ it ( 'passes mediation + signal through to navigator.credentials.get when supported' , async ( ) => {
176+ // eslint-disable-next-line
177+ const step = createJourneyStep ( webAuthnAuthMetaCallback70 as any ) ;
178+ const hiddenCallback = WebAuthn . getOutcomeCallback ( step ) ;
179+ if ( ! hiddenCallback ) throw new Error ( 'Missing hidden callback for test' ) ;
180+
181+ const abortController = new AbortController ( ) ;
182+ const credentialsGet = vi
183+ . spyOn ( navigator . credentials , 'get' )
184+ . mockResolvedValue ( { } as unknown as Credential ) ;
185+
186+ const outcomeSpy = vi
187+ . spyOn ( WebAuthn , 'getAuthenticationOutcome' )
188+ . mockReturnValue ( 'ok' as unknown as ReturnType < typeof WebAuthn . getAuthenticationOutcome > ) ;
189+
190+ await WebAuthn . authenticate ( step , 'conditional' , abortController . signal ) ;
191+
192+ expect ( outcomeSpy ) . toHaveBeenCalledTimes ( 1 ) ;
193+ expect ( credentialsGet ) . toHaveBeenCalledWith (
194+ expect . objectContaining ( {
195+ mediation : 'conditional' ,
196+ signal : abortController . signal ,
197+ publicKey : expect . any ( Object ) ,
198+ } ) ,
199+ ) ;
200+ expect ( hiddenCallback . getInputValue ( ) ) . toBe ( 'ok' ) ;
201+ } ) ;
202+ } ) ;
0 commit comments