22 * License, v. 2.0. If a copy of the MPL was not distributed with this
33 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44
5- import sinon from 'sinon' ;
65import { AppError as error } from '@fxa/accounts/errors' ;
76import * as authMethods from './authMethods' ;
87
98const MOCK_ACCOUNT = {
109 uid : 'abcdef123456' ,
1110} ;
1211
13- function mockDB ( ) {
14- return {
15- totpToken : sinon . stub ( ) ,
16- // Add other DB methods as needed
17- } ;
18- }
19-
2012describe ( 'availableAuthenticationMethods' , ( ) => {
21- let mockDbInstance : ReturnType < typeof mockDB > ;
13+ let db : { totpToken : jest . Mock } ;
2214
2315 beforeEach ( ( ) => {
24- mockDbInstance = mockDB ( ) ;
16+ db = { totpToken : jest . fn ( ) } ;
2517 } ) ;
2618
2719 it ( 'returns [`pwd`,`email`] for non-TOTP-enabled accounts' , async ( ) => {
28- mockDbInstance . totpToken = sinon . stub ( ) . rejects ( error . totpTokenNotFound ( ) ) ;
20+ db . totpToken . mockRejectedValue ( error . totpTokenNotFound ( ) ) ;
2921 const amr = await authMethods . availableAuthenticationMethods (
30- mockDbInstance as any ,
22+ db as any ,
3123 MOCK_ACCOUNT as any
3224 ) ;
33- expect ( mockDbInstance . totpToken . calledWithExactly ( MOCK_ACCOUNT . uid ) ) . toBe ( true ) ;
25+ expect ( db . totpToken ) . toHaveBeenCalledWith ( MOCK_ACCOUNT . uid ) ;
3426 expect ( Array . from ( amr ) . sort ( ) ) . toEqual ( [ 'email' , 'pwd' ] ) ;
3527 } ) ;
3628
3729 it ( 'returns [`pwd`,`email`,`otp`] for TOTP-enabled accounts' , async ( ) => {
38- mockDbInstance . totpToken = sinon . stub ( ) . resolves ( {
30+ db . totpToken . mockResolvedValue ( {
3931 verified : true ,
4032 enabled : true ,
4133 sharedSecret : 'secret!' ,
4234 } ) ;
4335 const amr = await authMethods . availableAuthenticationMethods (
44- mockDbInstance as any ,
36+ db as any ,
4537 MOCK_ACCOUNT as any
4638 ) ;
47- expect ( mockDbInstance . totpToken . calledWithExactly ( MOCK_ACCOUNT . uid ) ) . toBe ( true ) ;
39+ expect ( db . totpToken ) . toHaveBeenCalledWith ( MOCK_ACCOUNT . uid ) ;
4840 expect ( Array . from ( amr ) . sort ( ) ) . toEqual ( [ 'email' , 'otp' , 'pwd' ] ) ;
4941 } ) ;
5042
5143 it ( 'returns [`pwd`,`email`] when TOTP token is not yet enabled' , async ( ) => {
52- mockDbInstance . totpToken = sinon . stub ( ) . resolves ( {
44+ db . totpToken . mockResolvedValue ( {
5345 verified : true ,
5446 enabled : false ,
5547 sharedSecret : 'secret!' ,
5648 } ) ;
5749 const amr = await authMethods . availableAuthenticationMethods (
58- mockDbInstance as any ,
50+ db as any ,
5951 MOCK_ACCOUNT as any
6052 ) ;
61- expect ( mockDbInstance . totpToken . calledWithExactly ( MOCK_ACCOUNT . uid ) ) . toBe ( true ) ;
53+ expect ( db . totpToken ) . toHaveBeenCalledWith ( MOCK_ACCOUNT . uid ) ;
6254 expect ( Array . from ( amr ) . sort ( ) ) . toEqual ( [ 'email' , 'pwd' ] ) ;
6355 } ) ;
6456
6557 it ( 'rethrows unexpected DB errors' , async ( ) => {
66- mockDbInstance . totpToken = sinon . stub ( ) . rejects ( error . serviceUnavailable ( ) ) ;
67- try {
68- await authMethods . availableAuthenticationMethods (
69- mockDbInstance as any ,
70- MOCK_ACCOUNT as any
71- ) ;
72- throw new Error ( 'error should have been re-thrown' ) ;
73- } catch ( err : any ) {
74- expect ( mockDbInstance . totpToken . calledWithExactly ( MOCK_ACCOUNT . uid ) ) . toBe ( true ) ;
75- expect ( err . errno ) . toBe ( error . ERRNO . SERVER_BUSY ) ;
76- }
58+ db . totpToken . mockRejectedValue ( error . serviceUnavailable ( ) ) ;
59+ await expect (
60+ authMethods . availableAuthenticationMethods ( db as any , MOCK_ACCOUNT as any )
61+ ) . rejects . toMatchObject ( { errno : error . ERRNO . SERVER_BUSY } ) ;
62+ expect ( db . totpToken ) . toHaveBeenCalledWith ( MOCK_ACCOUNT . uid ) ;
7763 } ) ;
7864} ) ;
7965
@@ -98,6 +84,10 @@ describe('verificationMethodToAMR', () => {
9884 expect ( authMethods . verificationMethodToAMR ( 'recovery-code' ) ) . toBe ( 'otp' ) ;
9985 } ) ;
10086
87+ it ( 'maps `passkey` to `webauthn`' , ( ) => {
88+ expect ( authMethods . verificationMethodToAMR ( 'passkey' ) ) . toBe ( 'webauthn' ) ;
89+ } ) ;
90+
10191 it ( 'throws when given an unknown verification method' , ( ) => {
10292 expect ( ( ) => {
10393 authMethods . verificationMethodToAMR ( 'email-gotcha' as any ) ;
@@ -130,4 +120,63 @@ describe('maximumAssuranceLevel', () => {
130120 it ( 'returns 2 when both `pwd` and `otp` methods are used' , ( ) => {
131121 expect ( authMethods . maximumAssuranceLevel ( [ 'pwd' , 'otp' ] ) ) . toBe ( 2 ) ;
132122 } ) ;
123+
124+ it ( 'returns 2 when both `pwd` and `webauthn` methods are used (passkey session)' , ( ) => {
125+ expect ( authMethods . maximumAssuranceLevel ( [ 'pwd' , 'webauthn' ] ) ) . toBe ( 2 ) ;
126+ } ) ;
127+ } ) ;
128+
129+ describe ( 'accountRequiresAAL2' , ( ) => {
130+ let db : { totpToken : jest . Mock } ;
131+
132+ beforeEach ( ( ) => {
133+ db = { totpToken : jest . fn ( ) } ;
134+ } ) ;
135+
136+ it ( 'returns false when account has no TOTP token' , async ( ) => {
137+ db . totpToken . mockRejectedValue ( error . totpTokenNotFound ( ) ) ;
138+ const result = await authMethods . accountRequiresAAL2 (
139+ db as any ,
140+ MOCK_ACCOUNT as any
141+ ) ;
142+ expect ( result ) . toBe ( false ) ;
143+ } ) ;
144+
145+ // The current TOTP setup flow writes to the DB only at setup-complete via
146+ // replaceTotpToken, always with both flags true — partial states cannot be
147+ // produced by any current code path. These tests are defensive guards against
148+ // legacy data or future regressions.
149+ it ( 'returns false when TOTP token exists but is not verified' , async ( ) => {
150+ db . totpToken . mockResolvedValue ( { verified : false , enabled : true } ) ;
151+ const result = await authMethods . accountRequiresAAL2 (
152+ db as any ,
153+ MOCK_ACCOUNT as any
154+ ) ;
155+ expect ( result ) . toBe ( false ) ;
156+ } ) ;
157+
158+ it ( 'returns false when TOTP token exists but is not enabled' , async ( ) => {
159+ db . totpToken . mockResolvedValue ( { verified : true , enabled : false } ) ;
160+ const result = await authMethods . accountRequiresAAL2 (
161+ db as any ,
162+ MOCK_ACCOUNT as any
163+ ) ;
164+ expect ( result ) . toBe ( false ) ;
165+ } ) ;
166+
167+ it ( 'returns true when TOTP token is both verified and enabled' , async ( ) => {
168+ db . totpToken . mockResolvedValue ( { verified : true , enabled : true } ) ;
169+ const result = await authMethods . accountRequiresAAL2 (
170+ db as any ,
171+ MOCK_ACCOUNT as any
172+ ) ;
173+ expect ( result ) . toBe ( true ) ;
174+ } ) ;
175+
176+ it ( 'rethrows unexpected DB errors' , async ( ) => {
177+ db . totpToken . mockRejectedValue ( error . serviceUnavailable ( ) ) ;
178+ await expect (
179+ authMethods . accountRequiresAAL2 ( db as any , MOCK_ACCOUNT as any )
180+ ) . rejects . toMatchObject ( { errno : error . ERRNO . SERVER_BUSY } ) ;
181+ } ) ;
133182} ) ;
0 commit comments