@@ -189,6 +189,66 @@ def add_webauthn_credential(user)
189189 post :create , params : { credential : new_webauthn_credential , nickname : nickname }
190190 end . to_not change ( user , :webauthn_id )
191191 end
192+
193+ context 'when user has already enabled 2FA' do
194+ before do
195+ user . update ( webauthn_id : WebAuthn . generate_user_id )
196+ Fabricate ( :webauthn_credential , user_id : user . id , nickname : 'USB Key 2' )
197+ user . otp_secret = User . generate_otp_secret ( 32 )
198+ user . generate_otp_backup_codes!
199+ user . save
200+ end
201+
202+ it 'does not update user secret' do
203+ controller . session [ :webauthn_challenge ] = challenge
204+
205+ expect do
206+ post :create , params : { credential : new_webauthn_credential , nickname : nickname }
207+ end . to ( not_change { user . reload . otp_secret } )
208+ end
209+
210+ it 'does not change backup codes' do
211+ controller . session [ :webauthn_challenge ] = challenge
212+
213+ expect do
214+ post :create , params : { credential : new_webauthn_credential , nickname : nickname }
215+ end . to ( not_change { user . reload . otp_backup_codes } )
216+ end
217+
218+ it 'redirects to two factor authentication methods index' do
219+ controller . session [ :webauthn_challenge ] = challenge
220+
221+ post :create , params : { credential : new_webauthn_credential , nickname : nickname }
222+
223+ expect ( response . parsed_body [ 'redirect_path' ] ) . to eq settings_two_factor_authentication_methods_path
224+ end
225+ end
226+
227+ context 'when user has not enabled 2FA yet' do
228+ it 'updates user secret' do
229+ controller . session [ :webauthn_challenge ] = challenge
230+
231+ expect do
232+ post :create , params : { credential : new_webauthn_credential , nickname : nickname }
233+ end . to ( change { user . reload . otp_secret } )
234+ end
235+
236+ it 'generates backup codes' do
237+ controller . session [ :webauthn_challenge ] = challenge
238+
239+ expect do
240+ post :create , params : { credential : new_webauthn_credential , nickname : nickname }
241+ end . to ( change { user . reload . otp_backup_codes } )
242+ end
243+
244+ it "returns backup codes page in response's html_data attribute" do
245+ controller . session [ :webauthn_challenge ] = challenge
246+
247+ post :create , params : { credential : new_webauthn_credential , nickname : nickname }
248+
249+ expect ( response . parsed_body [ 'html_data' ] ) . to match ( /recovery codes/ )
250+ end
251+ end
192252 end
193253
194254 context 'when the nickname is already used' do
@@ -257,18 +317,68 @@ def add_webauthn_credential(user)
257317 add_webauthn_credential ( user )
258318 end
259319
260- context 'when deletion succeeds' do
261- it 'redirects to 2FA methods list and shows flash success' do
320+ it 'redirects to 2FA methods list and shows flash success' do
321+ delete :destroy , params : { id : user . webauthn_credentials . take . id }
322+
323+ expect ( response ) . to redirect_to settings_two_factor_authentication_methods_path
324+ expect ( flash [ :success ] ) . to be_present
325+ end
326+
327+ it 'deletes the credential' do
328+ expect do
262329 delete :destroy , params : { id : user . webauthn_credentials . take . id }
330+ end . to change { user . webauthn_credentials . count } . by ( -1 )
331+ end
263332
264- expect ( response ) . to redirect_to settings_two_factor_authentication_methods_path
265- expect ( flash [ :success ] ) . to be_present
333+ context "when deleted credential was not user's last webauthn credential" do
334+ before do
335+ Fabricate ( :webauthn_credential , user_id : user . id )
266336 end
267337
268- it 'deletes the credential' do
269- expect do
338+ context 'when user has OTP enabled' do
339+ before do
340+ user . update ( otp_required_for_login : true , otp_secret : User . generate_otp_secret ( 32 ) )
341+ user . generate_otp_backup_codes!
342+ user . save
343+ end
344+
345+ it 'does not disable 2FA' do
346+ expect_any_instance_of ( User ) . to_not receive ( :disable_two_factor! ) # rubocop:disable RSpec/AnyInstance
347+
348+ delete :destroy , params : { id : user . webauthn_credentials . take . id }
349+ end
350+ end
351+
352+ context 'when user does not have OTP enabled' do
353+ it 'does not disable 2FA' do
354+ expect_any_instance_of ( User ) . to_not receive ( :disable_two_factor! ) # rubocop:disable RSpec/AnyInstance
355+
270356 delete :destroy , params : { id : user . webauthn_credentials . take . id }
271- end . to change { user . webauthn_credentials . count } . by ( -1 )
357+ end
358+ end
359+ end
360+
361+ context "when deleted credential was user's last webauthn credential" do
362+ context 'when user has OTP enabled' do
363+ before do
364+ user . update ( otp_required_for_login : true , otp_secret : User . generate_otp_secret ( 32 ) )
365+ user . generate_otp_backup_codes!
366+ user . save
367+ end
368+
369+ it 'does not disable 2FA' do
370+ expect_any_instance_of ( User ) . to_not receive ( :disable_two_factor! ) # rubocop:disable RSpec/AnyInstance
371+
372+ delete :destroy , params : { id : user . webauthn_credentials . take . id }
373+ end
374+ end
375+
376+ context 'when user does not have OTP enabled' do
377+ it 'disables 2FA' do
378+ expect_any_instance_of ( User ) . to receive ( :disable_two_factor! ) # rubocop:disable RSpec/AnyInstance
379+
380+ delete :destroy , params : { id : user . webauthn_credentials . take . id }
381+ end
272382 end
273383 end
274384 end
0 commit comments