@@ -157,6 +157,7 @@ def test_post_validate_redirects(
157
157
user_service = pretend .stub (
158
158
find_userid = pretend .call_recorder (lambda username : user_id ),
159
159
update_user = pretend .call_recorder (lambda * a , ** kw : None ),
160
+ has_two_factor = lambda userid : False ,
160
161
)
161
162
breach_service = pretend .stub (check_password = lambda password , tags = None : False )
162
163
@@ -220,6 +221,9 @@ def test_post_validate_redirects(
220
221
221
222
assert remember .calls == [pretend .call (pyramid_request , str (user_id ))]
222
223
assert pyramid_request .session .invalidate .calls == [pretend .call ()]
224
+ assert pyramid_request .find_service .calls == [
225
+ pretend .call (IUserService , context = None ),
226
+ ]
223
227
assert pyramid_request .session .new_csrf_token .calls == [pretend .call ()]
224
228
225
229
@pytest .mark .parametrize (
@@ -234,6 +238,7 @@ def test_post_validate_no_redirects(
234
238
user_service = pretend .stub (
235
239
find_userid = pretend .call_recorder (lambda username : 1 ),
236
240
update_user = lambda * a , ** k : None ,
241
+ has_two_factor = lambda userid : False ,
237
242
)
238
243
breach_service = pretend .stub (check_password = lambda password , tags = None : False )
239
244
@@ -264,6 +269,189 @@ def test_redirect_authenticated_user(self):
264
269
assert isinstance (result , HTTPSeeOther )
265
270
assert result .headers ["Location" ] == "/the-redirect"
266
271
272
+ @pytest .mark .parametrize ("redirect_url" , ["test_redirect_url" , None ])
273
+ def test_two_factor_auth (self , pyramid_request , redirect_url , token_service ):
274
+ user_service = pretend .stub (
275
+ find_userid = pretend .call_recorder (lambda username : 1 ),
276
+ update_user = lambda * a , ** k : None ,
277
+ has_two_factor = lambda userid : True ,
278
+ )
279
+
280
+ token_service = pretend .stub (
281
+ dumps = pretend .call_recorder (lambda token_data : "test-secure-token" )
282
+ )
283
+ pyramid_request .find_service = lambda interface , ** kwargs : {
284
+ IUserService : user_service ,
285
+ ITokenService : token_service ,
286
+ }[interface ]
287
+
288
+ pyramid_request .method = "POST"
289
+ if redirect_url :
290
+ pyramid_request .POST ["next" ] = redirect_url
291
+
292
+ form_obj = pretend .stub (
293
+ validate = pretend .call_recorder (lambda : True ),
294
+ username = pretend .stub (data = "theuser" ),
295
+ )
296
+ form_class = pretend .call_recorder (lambda d , user_service : form_obj )
297
+ pyramid_request .route_path = pretend .call_recorder (
298
+ lambda a : "/account/two-factor"
299
+ )
300
+ result = views .login (pyramid_request , _form_class = form_class )
301
+
302
+ token_expected_data = {"userid" : 1 }
303
+ if redirect_url :
304
+ token_expected_data ['redirect_to' ] = redirect_url
305
+ assert token_service .dumps .calls == [pretend .call (token_expected_data )]
306
+
307
+ assert isinstance (result , HTTPSeeOther )
308
+ assert result .headerlist == [
309
+ ('Content-Type' , 'text/html; charset=UTF-8' ),
310
+ ('Content-Length' , '0' ),
311
+ ('Location' , '/account/two-factor' ),
312
+ ('Set-Cookie' , "{}={}; Path=/" .
313
+ format (views .TWO_FACTOR_COOKIE_KEY , "test-secure-token" ))]
314
+
315
+
316
+ class TestTwoFactor :
317
+ @pytest .mark .parametrize ("redirect_url" , [None , "/foo/bar/" , "/wat/" ])
318
+ def test_get_returns_form (self , pyramid_request , redirect_url ):
319
+ user_service = pretend .stub (
320
+ find_userid = pretend .call_recorder (lambda username : 1 ),
321
+ update_user = lambda * a , ** k : None ,
322
+ send_otp_secret = pretend .call_recorder (lambda userid : None ),
323
+ )
324
+
325
+ token_data = {"userid" : 1 }
326
+ if redirect_url :
327
+ token_data ['redirect_to' ] = redirect_url
328
+
329
+ token_service = pretend .stub (
330
+ loads = pretend .call_recorder (lambda token : token_data )
331
+ )
332
+ pyramid_request .find_service = lambda interface , ** kwargs : {
333
+ ITokenService : token_service ,
334
+ IUserService : user_service ,
335
+ }[interface ]
336
+
337
+ form_obj = pretend .stub ()
338
+ form_class = pretend .call_recorder (lambda d , user_service : form_obj )
339
+
340
+ result = views .two_factor (pyramid_request , _form_class = form_class )
341
+
342
+ assert result == {
343
+ "form" : form_obj ,
344
+ }
345
+ assert user_service .send_otp_secret .calls == [
346
+ pretend .call (1 )
347
+ ]
348
+ assert form_class .calls == [
349
+ pretend .call (pyramid_request .POST , user_service = user_service )
350
+ ]
351
+
352
+ @pytest .mark .parametrize ("redirect_url" , ["test_redirect_url" , None ])
353
+ def test_two_factor_auth (self , monkeypatch , pyramid_request , redirect_url ,
354
+ token_service ):
355
+ remember = pretend .call_recorder (lambda request , user_id : [("foo" , "bar" )])
356
+ monkeypatch .setattr (views , "remember" , remember )
357
+ user_service = pretend .stub (
358
+ find_userid = pretend .call_recorder (lambda username : 1 ),
359
+ update_user = lambda * a , ** k : None ,
360
+ send_otp_secret = lambda userid : None ,
361
+ check_otp_secret = lambda userid , otp_token : True ,
362
+ )
363
+
364
+ new_session = {}
365
+
366
+ token_data = {"userid" : str (1 )}
367
+ if redirect_url :
368
+ token_data ['redirect_to' ] = redirect_url
369
+
370
+ token_service = pretend .stub (
371
+ loads = pretend .call_recorder (lambda token : token_data )
372
+ )
373
+ pyramid_request .find_service = lambda interface , ** kwargs : {
374
+ IUserService : user_service ,
375
+ ITokenService : token_service ,
376
+ }[interface ]
377
+
378
+ pyramid_request .method = "POST"
379
+ pyramid_request .session = pretend .stub (
380
+ items = lambda : [("a" , "b" ), ("foo" , "bar" )],
381
+ update = new_session .update ,
382
+ invalidate = pretend .call_recorder (lambda : None ),
383
+ new_csrf_token = pretend .call_recorder (lambda : None ),
384
+ )
385
+
386
+ pyramid_request .set_property (
387
+ lambda r : str (uuid .uuid4 ()),
388
+ name = "unauthenticated_userid" ,
389
+ )
390
+
391
+ form_obj = pretend .stub (
392
+ validate = pretend .call_recorder (lambda : True ),
393
+ otp_secret = pretend .stub (data = "test-otp-secret" ),
394
+ )
395
+ form_class = pretend .call_recorder (lambda d , user_service : form_obj )
396
+ pyramid_request .route_path = pretend .call_recorder (
397
+ lambda a : "/account/two-factor"
398
+ )
399
+ pyramid_request .cookies [views .TWO_FACTOR_COOKIE_KEY ] = "test-secure-token"
400
+ result = views .two_factor (pyramid_request , _form_class = form_class )
401
+
402
+ token_expected_data = {"userid" : str (1 )}
403
+ if redirect_url :
404
+ token_expected_data ['redirect_to' ] = redirect_url
405
+ assert token_service .loads .calls == [pretend .call ("test-secure-token" )]
406
+
407
+ assert isinstance (result , HTTPSeeOther )
408
+
409
+ assert remember .calls == [pretend .call (pyramid_request , str (1 ))]
410
+ assert pyramid_request .session .invalidate .calls == [pretend .call ()]
411
+ assert pyramid_request .session .new_csrf_token .calls == [pretend .call ()]
412
+
413
+ @pytest .mark .parametrize ("redirect_url" , ["test_redirect_url" , None ])
414
+ def test_two_factor_auth_invalid (self , pyramid_request , redirect_url ,
415
+ token_service ):
416
+ user_service = pretend .stub (
417
+ find_userid = pretend .call_recorder (lambda username : 1 ),
418
+ update_user = lambda * a , ** k : None ,
419
+ send_otp_secret = lambda userid : None ,
420
+ check_otp_secret = lambda userid , otp_token : False ,
421
+ )
422
+
423
+ token_data = {"userid" : str (1 )}
424
+ if redirect_url :
425
+ token_data ['redirect_to' ] = redirect_url
426
+
427
+ token_service = pretend .stub (loads = pretend .call_recorder (
428
+ lambda token : token_data )
429
+ )
430
+ pyramid_request .find_service = lambda interface , ** kwargs : {
431
+ IUserService : user_service ,
432
+ ITokenService : token_service ,
433
+ }[interface ]
434
+
435
+ pyramid_request .method = "POST"
436
+
437
+ form_obj = pretend .stub (
438
+ validate = pretend .call_recorder (lambda : True ),
439
+ otp_secret = pretend .stub (data = "test-otp-secret" ),
440
+ )
441
+ form_class = pretend .call_recorder (lambda d , user_service : form_obj )
442
+ pyramid_request .route_path = pretend .call_recorder (
443
+ lambda a : "/account/two-factor"
444
+ )
445
+ pyramid_request .cookies [views .TWO_FACTOR_COOKIE_KEY ] = "test-secure-token"
446
+ result = views .two_factor (pyramid_request , _form_class = form_class )
447
+
448
+ token_expected_data = {"userid" : str (1 )}
449
+ if redirect_url :
450
+ token_expected_data ['redirect_to' ] = redirect_url
451
+ assert token_service .loads .calls == [pretend .call ("test-secure-token" )]
452
+
453
+ assert isinstance (result , HTTPSeeOther )
454
+
267
455
268
456
class TestLogout :
269
457
@pytest .mark .parametrize ("next_url" , [None , "/foo/bar/" , "/wat/" ])
@@ -729,7 +917,6 @@ def test_reset_password(self, db_request, user_service, token_service):
729
917
pretend .call (IUserService , context = None ),
730
918
pretend .call (IPasswordBreachedService , context = None ),
731
919
pretend .call (ITokenService , name = "password" ),
732
- pretend .call (IUserService , context = None ),
733
920
]
734
921
735
922
@pytest .mark .parametrize (
0 commit comments