2023-05-29 18:16:59 +00:00
import datetime
2024-11-08 14:15:12 +00:00
from unittest import mock
2023-05-29 18:16:59 +00:00
2023-08-03 16:48:51 +00:00
import pytest
2023-05-29 18:16:59 +00:00
import wtforms
from babel . dates import LOCALTZ
from flask import current_app
from werkzeug . datastructures import ImmutableMultiDict
2024-03-15 18:58:06 +00:00
from canaille . app . forms import DateTimeUTCField
2024-11-07 14:51:21 +00:00
from canaille . app . forms import compromised_password_validator
2024-10-28 21:17:47 +00:00
from canaille . app . forms import password_length_validator
from canaille . app . forms import password_too_long_validator
2024-03-15 18:58:06 +00:00
from canaille . app . forms import phone_number
2023-05-29 18:16:59 +00:00
def test_datetime_utc_field_no_timezone_is_local_timezone ( testclient ) :
2023-12-18 17:06:03 +00:00
current_app . config [ " CANAILLE " ] [ " TIMEZONE " ] = None
2023-05-29 18:16:59 +00:00
class TestForm ( wtforms . Form ) :
dt = DateTimeUTCField ( )
form = TestForm ( )
form . validate ( )
assert form . dt . data is None
utc_date = datetime . datetime ( 2023 , 6 , 1 , 12 , tzinfo = datetime . timezone . utc )
2023-11-16 16:42:04 +00:00
offset = LOCALTZ . utcoffset ( utc_date . replace ( tzinfo = None ) )
2023-05-29 18:16:59 +00:00
locale_date = datetime . datetime ( 2023 , 6 , 1 , 12 ) + offset
rendered_locale_date = locale_date . strftime ( " % Y- % m- %d % H: % M: % S " )
rendered_locale_date_form = locale_date . strftime ( " % Y- % m- %d % H: % M: % S " )
request_form = ImmutableMultiDict ( { " dt " : rendered_locale_date_form } )
form = TestForm ( request_form )
assert form . validate ( )
assert form . dt . data == utc_date
assert (
form . dt ( )
== f ' <input id= " dt " name= " dt " type= " datetime-local " value= " { rendered_locale_date_form } " > '
)
form = TestForm ( data = { " dt " : utc_date } )
assert form . validate ( )
assert form . dt . data == utc_date
assert (
form . dt ( )
== f ' <input id= " dt " name= " dt " type= " datetime-local " value= " { rendered_locale_date } " > '
)
class Foobar :
dt = utc_date
form = TestForm ( obj = Foobar ( ) )
assert form . validate ( )
assert form . dt . data == utc_date
assert (
form . dt ( )
== f ' <input id= " dt " name= " dt " type= " datetime-local " value= " { rendered_locale_date } " > '
)
def test_datetime_utc_field_utc ( testclient ) :
2023-12-18 17:06:03 +00:00
current_app . config [ " CANAILLE " ] [ " TIMEZONE " ] = " UTC "
2023-05-29 18:16:59 +00:00
class TestForm ( wtforms . Form ) :
dt = DateTimeUTCField ( )
form = TestForm ( )
form . validate ( )
assert form . dt . data is None
date = datetime . datetime ( 2023 , 6 , 1 , 12 , tzinfo = datetime . timezone . utc )
rendered_date = date . strftime ( " % Y- % m- %d % H: % M: % S " )
rendered_date_form = date . strftime ( " % Y- % m- %d % H: % M: % S " )
request_form = ImmutableMultiDict ( { " dt " : rendered_date_form } )
form = TestForm ( request_form )
assert form . validate ( )
assert form . dt . data == date
assert (
form . dt ( )
== f ' <input id= " dt " name= " dt " type= " datetime-local " value= " { rendered_date_form } " > '
)
form = TestForm ( data = { " dt " : date } )
assert form . validate ( )
assert form . dt . data == date
assert (
form . dt ( )
== f ' <input id= " dt " name= " dt " type= " datetime-local " value= " { rendered_date } " > '
)
class Foobar :
dt = date
form = TestForm ( obj = Foobar ( ) )
assert form . validate ( )
assert form . dt . data == date
assert (
form . dt ( )
== f ' <input id= " dt " name= " dt " type= " datetime-local " value= " { rendered_date } " > '
)
def test_datetime_utc_field_japan_timezone ( testclient ) :
2023-12-18 17:06:03 +00:00
current_app . config [ " CANAILLE " ] [ " TIMEZONE " ] = " Japan "
2023-05-29 18:16:59 +00:00
class TestForm ( wtforms . Form ) :
dt = DateTimeUTCField ( )
form = TestForm ( )
form . validate ( )
assert form . dt . data is None
utc_date = datetime . datetime ( 2023 , 6 , 1 , 12 , tzinfo = datetime . timezone . utc )
locale_date = datetime . datetime ( 2023 , 6 , 1 , 21 )
rendered_locale_date = locale_date . strftime ( " % Y- % m- %d % H: % M: % S " )
rendered_locale_date_form = locale_date . strftime ( " % Y- % m- %d % H: % M: % S " )
request_form = ImmutableMultiDict ( { " dt " : rendered_locale_date_form } )
form = TestForm ( request_form )
assert form . validate ( )
assert form . dt . data == utc_date
assert (
form . dt ( )
== f ' <input id= " dt " name= " dt " type= " datetime-local " value= " { rendered_locale_date_form } " > '
)
form = TestForm ( data = { " dt " : utc_date } )
assert form . validate ( )
assert form . dt . data == utc_date
assert (
form . dt ( )
== f ' <input id= " dt " name= " dt " type= " datetime-local " value= " { rendered_locale_date } " > '
)
class Foobar :
dt = utc_date
form = TestForm ( obj = Foobar ( ) )
assert form . validate ( )
assert form . dt . data == utc_date
assert (
form . dt ( )
== f ' <input id= " dt " name= " dt " type= " datetime-local " value= " { rendered_locale_date } " > '
)
def test_datetime_utc_field_invalid_timezone ( testclient ) :
2023-12-18 17:06:03 +00:00
current_app . config [ " CANAILLE " ] [ " TIMEZONE " ] = " invalid "
2023-05-29 18:16:59 +00:00
class TestForm ( wtforms . Form ) :
dt = DateTimeUTCField ( )
form = TestForm ( )
form . validate ( )
assert form . dt . data is None
utc_date = datetime . datetime ( 2023 , 6 , 1 , 12 , tzinfo = datetime . timezone . utc )
2023-11-16 16:42:04 +00:00
offset = LOCALTZ . utcoffset ( utc_date . replace ( tzinfo = None ) )
2023-05-29 18:16:59 +00:00
locale_date = datetime . datetime ( 2023 , 6 , 1 , 12 ) + offset
rendered_locale_date = locale_date . strftime ( " % Y- % m- %d % H: % M: % S " )
rendered_locale_date_form = locale_date . strftime ( " % Y- % m- %d % H: % M: % S " )
request_form = ImmutableMultiDict ( { " dt " : rendered_locale_date_form } )
form = TestForm ( request_form )
assert form . validate ( )
assert form . dt . data == utc_date
assert (
form . dt ( )
== f ' <input id= " dt " name= " dt " type= " datetime-local " value= " { rendered_locale_date_form } " > '
)
form = TestForm ( data = { " dt " : utc_date } )
assert form . validate ( )
assert form . dt . data == utc_date
assert (
form . dt ( )
== f ' <input id= " dt " name= " dt " type= " datetime-local " value= " { rendered_locale_date } " > '
)
class Foobar :
dt = utc_date
form = TestForm ( obj = Foobar ( ) )
assert form . validate ( )
assert form . dt . data == utc_date
assert (
form . dt ( )
== f ' <input id= " dt " name= " dt " type= " datetime-local " value= " { rendered_locale_date } " > '
)
2023-06-22 09:39:50 +00:00
2024-04-14 20:51:58 +00:00
def test_fieldlist_add_readonly ( testclient , logged_user , backend ) :
2023-12-18 17:06:03 +00:00
testclient . app . config [ " CANAILLE " ] [ " ACL " ] [ " DEFAULT " ] [ " WRITE " ] . remove ( " phone_numbers " )
testclient . app . config [ " CANAILLE " ] [ " ACL " ] [ " DEFAULT " ] [ " READ " ] . append ( " phone_numbers " )
2024-04-14 20:51:58 +00:00
backend . reload ( logged_user )
2023-06-28 11:26:15 +00:00
res = testclient . get ( " /profile/user " )
2023-07-20 16:43:28 +00:00
form = res . forms [ " baseform " ]
assert " readonly " in form [ " phone_numbers-0 " ] . attrs
assert " phone_numbers-1 " not in form . fields
2023-06-28 11:26:15 +00:00
data = {
2023-07-20 16:43:28 +00:00
" csrf_token " : form [ " csrf_token " ] . value ,
" family_name " : form [ " family_name " ] . value ,
" phone_numbers-0 " : form [ " phone_numbers-0 " ] . value ,
2023-06-28 11:26:15 +00:00
" fieldlist_add " : " phone_numbers-0 " ,
}
testclient . post ( " /profile/user " , data , status = 403 )
2024-04-14 18:31:43 +00:00
def test_fieldlist_remove_readonly ( testclient , logged_user , backend ) :
2023-12-18 17:06:03 +00:00
testclient . app . config [ " CANAILLE " ] [ " ACL " ] [ " DEFAULT " ] [ " WRITE " ] . remove ( " phone_numbers " )
testclient . app . config [ " CANAILLE " ] [ " ACL " ] [ " DEFAULT " ] [ " READ " ] . append ( " phone_numbers " )
2024-04-14 20:51:58 +00:00
backend . reload ( logged_user )
2023-11-24 11:10:17 +00:00
2023-06-28 11:26:15 +00:00
logged_user . phone_numbers = [ " 555-555-000 " , " 555-555-111 " ]
2024-04-14 18:31:43 +00:00
backend . save ( logged_user )
2023-06-28 11:26:15 +00:00
res = testclient . get ( " /profile/user " )
2023-07-20 16:43:28 +00:00
form = res . forms [ " baseform " ]
assert " readonly " in form [ " phone_numbers-0 " ] . attrs
assert " readonly " in form [ " phone_numbers-1 " ] . attrs
2023-06-28 11:26:15 +00:00
data = {
2023-07-20 16:43:28 +00:00
" csrf_token " : form [ " csrf_token " ] . value ,
" family_name " : form [ " family_name " ] . value ,
" phone_numbers-0 " : form [ " phone_numbers-0 " ] . value ,
2023-06-28 11:26:15 +00:00
" fieldlist_remove " : " phone_numbers-1 " ,
}
testclient . post ( " /profile/user " , data , status = 403 )
2023-06-22 09:39:50 +00:00
def test_inline_validation_invalid_field ( testclient , logged_admin , user ) :
res = testclient . get ( " /profile " )
testclient . post (
" /profile " ,
{
" csrf_token " : res . form [ " csrf_token " ] . value ,
" email " : " john@doe.com " ,
} ,
headers = {
" HX-Request " : " true " ,
" HX-Trigger-Name " : " invalid-field " ,
} ,
status = 400 ,
)
2023-08-03 16:48:51 +00:00
def test_phone_number_validator ( ) :
class Field :
def __init__ ( self , data ) :
self . data = data
phone_number ( None , Field ( " 0601060106 " ) )
phone_number ( None , Field ( " 06 01 06 01 06 " ) )
phone_number ( None , Field ( " 06 01 06 01 06 " ) )
phone_number ( None , Field ( " 06-01-06-01-06 " ) )
phone_number ( None , Field ( " 06.01.06.01.06 " ) )
phone_number ( None , Field ( " +336 01 06 01 06 " ) )
phone_number ( None , Field ( " 555-000-555 " ) )
with pytest . raises ( wtforms . ValidationError ) :
phone_number ( None , Field ( " invalid " ) )
2024-10-28 21:17:47 +00:00
def test_minimum_password_length_config ( testclient ) :
class Field :
def __init__ ( self , data ) :
self . data = data
current_app . config [ " CANAILLE " ] [ " MIN_PASSWORD_LENGTH " ] = 20
password_length_validator ( None , Field ( " 12345678901234567890 " ) )
with pytest . raises ( wtforms . ValidationError ) :
password_length_validator ( None , Field ( " 1234567890123456789 " ) )
current_app . config [ " CANAILLE " ] [ " MIN_PASSWORD_LENGTH " ] = 8
password_length_validator ( None , Field ( " 12345678 " ) )
with pytest . raises ( wtforms . ValidationError ) :
password_length_validator ( None , Field ( " 1234567 " ) )
with pytest . raises ( wtforms . ValidationError ) :
password_length_validator ( None , Field ( " 1 " ) )
current_app . config [ " CANAILLE " ] [ " MIN_PASSWORD_LENGTH " ] = 0
password_length_validator ( None , Field ( " " ) )
current_app . config [ " CANAILLE " ] [ " MIN_PASSWORD_LENGTH " ] = None
password_length_validator ( None , Field ( " " ) )
def test_password_strength_progress_bar ( testclient , logged_user ) :
res = testclient . get ( " /profile/user/settings " )
res = testclient . post (
" /profile/user/settings " ,
{
" csrf_token " : res . form [ " csrf_token " ] . value ,
2024-11-05 14:44:25 +00:00
" password1 " : " i ' m a little pea " ,
2024-10-28 21:17:47 +00:00
} ,
headers = {
" HX-Request " : " true " ,
" HX-Trigger-Name " : " password1 " ,
} ,
)
2024-11-05 14:44:25 +00:00
res . mustcontain ( ' data-percent= " 100 " ' )
2024-10-28 21:17:47 +00:00
def test_maximum_password_length_config ( testclient ) :
class Field :
def __init__ ( self , data ) :
self . data = data
password_too_long_validator ( None , Field ( " a " * 1000 ) )
with pytest . raises ( wtforms . ValidationError ) :
password_too_long_validator ( None , Field ( " a " * 1001 ) )
current_app . config [ " CANAILLE " ] [ " MAX_PASSWORD_LENGTH " ] = 500
password_too_long_validator ( None , Field ( " a " * 500 ) )
with pytest . raises ( wtforms . ValidationError ) :
password_too_long_validator ( None , Field ( " a " * 501 ) )
current_app . config [ " CANAILLE " ] [ " MAX_PASSWORD_LENGTH " ] = None
password_too_long_validator ( None , Field ( " a " * 4096 ) )
with pytest . raises ( wtforms . ValidationError ) :
password_too_long_validator ( None , Field ( " a " * 4097 ) )
current_app . config [ " CANAILLE " ] [ " MAX_PASSWORD_LENGTH " ] = 0
password_too_long_validator ( None , Field ( " a " * 4096 ) )
with pytest . raises ( wtforms . ValidationError ) :
password_too_long_validator ( None , Field ( " a " * 4097 ) )
current_app . config [ " CANAILLE " ] [ " MAX_PASSWORD_LENGTH " ] = 5000
password_too_long_validator ( None , Field ( " a " * 4096 ) )
with pytest . raises ( wtforms . ValidationError ) :
password_too_long_validator ( None , Field ( " a " * 4097 ) )
2024-11-07 14:51:21 +00:00
2024-11-14 14:32:31 +00:00
@mock.patch ( " requests.api.get " )
def test_compromised_password_validator ( api_get , testclient ) :
2024-11-14 15:10:43 +00:00
current_app . config [ " CANAILLE " ] [ " ENABLE_PASSWORD_COMPROMISSION_CHECK " ] = True
2024-11-15 11:22:10 +00:00
# This content simulates a result from the hibp api containing the suffixes of the following password hashes: 'password', '987654321', 'correct horse battery staple', 'zxcvbn123', 'azertyuiop123'
2024-11-14 14:32:31 +00:00
class Response :
content = b " 1E4C9B93F3F0682250B6CF8331B7EE68FD8:3 \r \n CAA6D483CC3887DCE9D1B8EB91408F1EA7A:3 \r \n AD6438836DBE526AA231ABDE2D0EEF74D42:3 \r \n 8289894DDB6317178960AB5AE98B81BBF97:1 \r \n 5FF0B6F9EAC40D5CA7B4DAA7B64F0E6F4AA:2 \r \n "
api_get . return_value = Response
2024-11-07 14:51:21 +00:00
class Field :
def __init__ ( self , data ) :
self . data = data
compromised_password_validator ( None , Field ( " i ' m a little pea " ) )
compromised_password_validator ( None , Field ( " i ' m a little chickpea " ) )
compromised_password_validator ( None , Field ( " i ' m singing in the rain " ) )
with pytest . raises ( wtforms . ValidationError ) :
compromised_password_validator ( None , Field ( " password " ) )
with pytest . raises ( wtforms . ValidationError ) :
compromised_password_validator ( None , Field ( " 987654321 " ) )
with pytest . raises ( wtforms . ValidationError ) :
compromised_password_validator ( None , Field ( " correct horse battery staple " ) )
with pytest . raises ( wtforms . ValidationError ) :
compromised_password_validator ( None , Field ( " zxcvbn123 " ) )
with pytest . raises ( wtforms . ValidationError ) :
compromised_password_validator ( None , Field ( " azertyuiop123 " ) )
2024-11-08 14:15:12 +00:00
2024-11-14 15:10:43 +00:00
current_app . config [ " CANAILLE " ] [ " ENABLE_PASSWORD_COMPROMISSION_CHECK " ] = False
assert compromised_password_validator ( None , Field ( " password " ) ) is None
2024-11-08 14:15:12 +00:00
@mock.patch ( " requests.api.get " )
def test_compromised_password_validator_with_failure_of_api_request_and_no_SMTP_in_config (
api_get , testclient , logged_user
) :
2024-11-14 15:10:43 +00:00
current_app . config [ " CANAILLE " ] [ " ENABLE_PASSWORD_COMPROMISSION_CHECK " ] = True
2024-11-08 14:15:12 +00:00
api_get . side_effect = mock . Mock ( side_effect = Exception ( ) )
current_app . config [ " CANAILLE " ] [ " SMTP " ] = None
class Field :
def __init__ ( self , data ) :
self . data = data
compromised_password_validator ( None , Field ( " i ' m a little pea " ) )
compromised_password_validator ( None , Field ( " i ' m a little chickpea " ) )
compromised_password_validator ( None , Field ( " i ' m singing in the rain " ) )
compromised_password_validator ( None , Field ( " password " ) )
compromised_password_validator ( None , Field ( " 987654321 " ) )
compromised_password_validator ( None , Field ( " correct horse battery staple " ) )
compromised_password_validator ( None , Field ( " zxcvbn123 " ) )
compromised_password_validator ( None , Field ( " azertyuiop123 " ) )
@mock.patch ( " requests.api.get " )
def test_compromised_password_validator_with_failure_of_api_request_and_only_with_htmx (
api_get , testclient , logged_user
) :
2024-11-14 15:10:43 +00:00
current_app . config [ " CANAILLE " ] [ " ENABLE_PASSWORD_COMPROMISSION_CHECK " ] = True
2024-11-08 14:15:12 +00:00
api_get . side_effect = mock . Mock ( side_effect = Exception ( ) )
res = testclient . get ( " /profile/user/settings " )
res = testclient . post (
" /profile/user/settings " ,
{
" csrf_token " : res . form [ " csrf_token " ] . value ,
" password1 " : " correct horse battery staple " ,
} ,
headers = {
" HX-Request " : " true " ,
" HX-Trigger-Name " : " password1 " ,
} ,
)
res . mustcontain ( ' data-percent= " 100 " ' )