Skip to content

Commit fc0ddfe

Browse files
authored
Dom pwd (#132)
* Fixed #127
1 parent 317a6c7 commit fc0ddfe

File tree

8 files changed

+69
-18
lines changed

8 files changed

+69
-18
lines changed

ptmd/api/queries/users.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ def create_user() -> tuple[Response, int]:
4646
return jsonify(user_dict), 200
4747
except IntegrityError:
4848
return jsonify({"msg": "Username or email already taken"}), 400
49+
except PasswordPolicyError as e:
50+
return jsonify({"msg": str(e)}), 400
51+
except Exception:
52+
return jsonify({"msg": "An unexpected error occurred"}), 500
4953

5054

5155
def login() -> tuple[Response, int]:

ptmd/database/const.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@
77

88
SQLALCHEMY_DATABASE_URI: str = DOT_ENV_CONFIG['SQLALCHEMY_DATABASE_URL']
99
SQLALCHEMY_SECRET_KEY: str = DOT_ENV_CONFIG['SQLALCHEMY_SECRET_KEY']
10-
PASSWORD_POLICY: str = "^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,20}$"
10+
PASSWORD_POLICY: str = r"^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[\[\]\(\)#?!@$%^&*-_+=<>:;,.]).{8,20}$"

ptmd/database/models/user.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ def __init__(
4848
) -> None:
4949
""" Constructor for the User class. Let's use encode the password with bcrypt before committing it to the
5050
database. """
51+
if not match(PASSWORD_POLICY, password):
52+
raise PasswordPolicyError
5153
self.username = username
5254
self.password = bcrypt.hash(password)
5355
self.email = email
@@ -104,7 +106,7 @@ def set_password(self, password: str) -> None:
104106
:raises PasswordPolicyError: if the password does not match the password policy
105107
"""
106108
if not match(PASSWORD_POLICY, password):
107-
raise PasswordPolicyError()
109+
raise PasswordPolicyError
108110
self.password = bcrypt.hash(password)
109111
session.commit()
110112

ptmd/database/queries/users.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,5 +55,6 @@ def get_token(token: str) -> Token:
5555
if token_from_db is None:
5656
raise TokenInvalidError
5757
if token_from_db.expires_on < datetime.now(token_from_db.expires_on.tzinfo):
58+
# session.delete(token_from_db) # type: ignore
5859
raise TokenExpiredError
5960
return token_from_db

tests/test_api/test_queries/test_users.py

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from sqlalchemy.exc import IntegrityError
77

88
from ptmd.api import app
9-
from ptmd.exceptions import PasswordPolicyError
9+
from ptmd.exceptions import PasswordPolicyError, TokenInvalidError, TokenExpiredError
1010

1111

1212
HEADERS = {'Content-Type': 'application/json'}
@@ -91,6 +91,33 @@ def test_create_user(self, mock_organisation, mock_user,
9191
data=dumps(user_data))
9292
self.assertEqual(created_user.json, {'msg': 'Username or email already taken'})
9393

94+
@patch('ptmd.api.queries.users.Organisation')
95+
def test_create_user_invalid_password(
96+
self, mock_organisation, mock_get_current_user, mock_verify_jwt, mock_verify_in_request):
97+
mock_get_current_user().role = 'admin'
98+
user_data = {
99+
"username": "1234",
100+
"password": "1234",
101+
"confirm_password": "1234",
102+
"organisation": "UOX",
103+
"email": "[email protected]"
104+
}
105+
with app.test_client() as client:
106+
response = client.post('/api/users', headers={'Authorization': f'Bearer {123}', **HEADERS},
107+
data=dumps(user_data))
108+
self.assertEqual(response.json, {'msg': 'Password must be between 8 and 20 characters long, contain at '
109+
'least one uppercase letter, one lowercase letter, one number '
110+
'and one special character.'})
111+
self.assertEqual(response.status_code, 400)
112+
113+
user_data['password'] = '!@#$%a^&A()a'
114+
user_data['confirm_password'] = '!@#$%a^&A()a'
115+
mock_organisation.query.filter.side_effect = Exception
116+
response = client.post('/api/users', headers={'Authorization': f'Bearer {123}', **HEADERS},
117+
data=dumps(user_data))
118+
self.assertEqual(response.json, {'msg': 'An unexpected error occurred'})
119+
self.assertEqual(response.status_code, 500)
120+
94121
@patch('ptmd.api.queries.users.session')
95122
@patch('ptmd.api.queries.users.get_jwt', return_value={'sub': 1})
96123
@patch('ptmd.api.queries.users.User')
@@ -283,17 +310,26 @@ def test_reset_password_failed(self, mock_get_current_user, mock_verify_jwt, moc
283310
@patch('ptmd.api.queries.users.get_token')
284311
def test_reset_password_error(self, mock_token,
285312
mock_get_current_user, mock_verify_jwt, mock_verify_in_request):
286-
mock_token.side_effect = PasswordPolicyError()
287-
headers = {'Authorization': f'Bearer {123}', **HEADERS}
313+
mock_token.return_value.user_reset[0].set_password.side_effect = PasswordPolicyError
314+
headers = {'Authorization': 'Bearer 123', **HEADERS}
288315
with app.test_client() as client:
289-
response = client.post('/api/users/reset/123', data=dumps({"password": "None"}), headers=headers)
316+
response = client.post('/api/users/reset/456', data=dumps({"password": "None"}), headers=headers)
290317
self.assertEqual(response.json, {"msg": "Password must be between 8 and 20 characters long, contain at "
291318
"least one uppercase letter, one lowercase letter, one number "
292319
"and one special character."})
293320
self.assertEqual(response.status_code, 400)
294321

295-
mock_token.side_effect = Exception()
296-
with app.test_client() as client:
322+
mock_token.side_effect = TokenInvalidError
323+
response = client.post('/api/users/reset/123', data=dumps({"password": "None"}), headers=headers)
324+
self.assertEqual(response.json, {"msg": "Invalid token"})
325+
self.assertEqual(response.status_code, 400)
326+
327+
mock_token.side_effect = TokenExpiredError
328+
response = client.post('/api/users/reset/123', data=dumps({"password": "None"}), headers=headers)
329+
self.assertEqual(response.json, {"msg": "Token expired"})
330+
self.assertEqual(response.status_code, 400)
331+
332+
mock_token.side_effect = Exception
297333
response = client.post('/api/users/reset/123', data=dumps({"password": "None"}), headers=headers)
298334
self.assertEqual(response.json, {"msg": "An unexpected error occurred"})
299335
self.assertEqual(response.status_code, 500)

tests/test_database/test_models/test_user.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def test_user_admin(self):
4040
organisation = Organisation(name='123', gdrive_id='1')
4141
organisation.files = []
4242
organisation.id = 2
43-
user = User(username='test', password='test', email='[email protected]', role='admin')
43+
user = User(username='test', password='!Str0?nkPassw0rd', email='[email protected]', role='admin')
4444
user.organisation = organisation
4545
self.assertEqual(user.role, 'admin')
4646
self.assertEqual(dict(user)['files'], [])
@@ -52,7 +52,7 @@ def test_user_with_organisation(self, mock_send_mail, mock_create_access_token):
5252
user_input: dict = {
5353
'username': 'rw1',
5454
'organisation_id': organisation.organisation_id,
55-
'password': 'test',
55+
'password': '!Str0?nkPassw0rd',
5656
'email': '[email protected]'
5757
}
5858
user = User(**user_input)
@@ -63,15 +63,15 @@ def test_user_with_organisation(self, mock_send_mail, mock_create_access_token):
6363
@patch('ptmd.database.models.user.session')
6464
@patch('ptmd.database.models.token.send_confirmation_mail', return_value=True)
6565
def test_set_role_success(self, mock_send_confirmation_mail, mock_session):
66-
user = User('test', 'test', 'test', 'disabled')
66+
user = User('test', '!Str0?nkPassw0rd', 'test', 'disabled')
6767
user.set_role('banned')
6868
self.assertEqual(user.role, 'banned')
6969
mock_session.commit.assert_called_once()
7070

7171
@patch('ptmd.database.models.user.session')
7272
@patch('ptmd.database.models.token.send_confirmation_mail', return_value=True)
7373
def test_set_role_invalid_role(self, mock_send_confirmation_mail, mock_session):
74-
user = User('test', 'test', 'test', 'disabled')
74+
user = User('test', '!Str0?nkPassw0rd', 'test', 'disabled')
7575
with self.assertRaises(ValueError) as context:
7676
user.set_role('invalid role')
7777
self.assertEqual(str(context.exception), "Invalid role: invalid role")
@@ -89,7 +89,7 @@ def test_user_serialisation_with_organisation(self, mock_organisation, mock_orga
8989
organisation = Organisation(name='123', gdrive_id='1')
9090
organisation.files = [file_1, file_2]
9191
organisation.id = 2
92-
user = User(username='test', password='test', email='[email protected]', role='admin')
92+
user = User(username='test', password='!Str0?nkPassw0rd', email='[email protected]', role='admin')
9393
user.organisation = organisation
9494
user.files = [file_1]
9595
files = dict(user)['files']
@@ -98,9 +98,17 @@ def test_user_serialisation_with_organisation(self, mock_organisation, mock_orga
9898

9999
@patch('ptmd.database.models.user.session')
100100
def test_set_password_policy_failure(self, mock_session):
101-
user = User(username='test', password='test', email='[email protected]', role='admin')
101+
user = User(username='test', password='!Str0?nkPassw0rd[]()', email='[email protected]', role='admin')
102102
with self.assertRaises(PasswordPolicyError) as context:
103103
user.set_password('test')
104104
self.assertEqual(str(context.exception),
105-
"Password must be between 8 and 20 characters long, contain at least one uppercase letter, one "
106-
"lowercase letter, one number and one special character.")
105+
"Password must be between 8 and 20 characters long, contain at least one uppercase letter, "
106+
"one lowercase letter, one number and one special character.")
107+
108+
def test_create_user_with_invalid_password(self):
109+
user = User(username='test', password=':AStr0nkP3Wd!!', email='[email protected]', role='admin')
110+
with self.assertRaises(PasswordPolicyError) as context:
111+
user.set_password('test')
112+
self.assertEqual(str(context.exception),
113+
"Password must be between 8 and 20 characters long, contain at least one uppercase letter, one"
114+
" lowercase letter, one number and one special character.")

tests/test_lib/test_email/test_core.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
class TestEmailCore(TestCase):
1212

1313
def test_send_validation_mail(self, mock_build, mock_get_config, mock_credentials):
14-
user = User(username='username', email='email', password='password')
14+
user = User(username='username', email='email', password='!Str0?nkPassw0rd')
1515
response = send_validation_mail(user)
1616
self.assertIn('<h1> Hello, admin </h1>', response)
1717

tests/test_lib/test_email/test_load_templates.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def test_validated_email(self):
2121

2222
def test_create_validation_mail_content(self):
2323
organisation = Organisation(name='ORGANISATION NAME')
24-
user = User(username='USERNAME', password='test', email='EMAIL', role='admin',
24+
user = User(username='USERNAME', password='!Str0?nkPassw0rd', email='EMAIL', role='admin',
2525
organisation_id=organisation.organisation_id)
2626
data = create_validation_mail_content(user)
2727
self.assertIn('USERNAME', data)

0 commit comments

Comments
 (0)