Skip to content

Commit ceae20b

Browse files
Tests of the password validation regex
1 parent 187d575 commit ceae20b

File tree

4 files changed

+146
-79
lines changed

4 files changed

+146
-79
lines changed

templates/authentication/register.html

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
<!-- Password Input -->
2525
<div class="mb-3">
2626
<label for="password" class="form-label">Password</label>
27-
<!-- Make sure 9g,X*88w[6"W and ^94cPSf2^)z2^,& pass validation -->
2827
<input type="password" class="form-control" id="password" name="password"
2928
pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[@$!%*?&\{\}\<\>\.\,\\\\'#\-_=\+\(\)\[\]:;\|~\/])[A-Za-z\d@$!%*?&\{\}\<\>\.\,\\\\'#\-_=\+\(\)\[\]:;\|~\/]{8,}"
3029
title="Must contain at least one number, one uppercase and lowercase letter, one special character, and at least 8 or more characters"

tests/test_auth.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import re
2+
import string
3+
import random
4+
from datetime import timedelta
5+
from urllib.parse import urlparse, parse_qs
6+
from starlette.datastructures import URLPath
7+
from main import app
8+
from utils.auth import (
9+
create_access_token,
10+
create_refresh_token,
11+
verify_password,
12+
get_password_hash,
13+
validate_token,
14+
generate_password_reset_url,
15+
PASSWORD_PATTERN
16+
)
17+
18+
19+
def test_password_hashing():
20+
password = "Test123!@#"
21+
hashed = get_password_hash(password)
22+
assert verify_password(password, hashed)
23+
assert not verify_password("wrong_password", hashed)
24+
25+
26+
def test_token_creation_and_validation():
27+
data = {"sub": "[email protected]"}
28+
29+
# Test access token
30+
access_token = create_access_token(data)
31+
decoded = validate_token(access_token, "access")
32+
assert decoded is not None
33+
assert decoded["sub"] == data["sub"]
34+
assert decoded["type"] == "access"
35+
36+
# Test refresh token
37+
refresh_token = create_refresh_token(data)
38+
decoded = validate_token(refresh_token, "refresh")
39+
assert decoded is not None
40+
assert decoded["sub"] == data["sub"]
41+
assert decoded["type"] == "refresh"
42+
43+
44+
def test_expired_token():
45+
data = {"sub": "[email protected]"}
46+
expired_delta = timedelta(minutes=-10)
47+
expired_token = create_access_token(data, expired_delta)
48+
decoded = validate_token(expired_token, "access")
49+
assert decoded is None
50+
51+
52+
def test_invalid_token_type():
53+
data = {"sub": "[email protected]"}
54+
access_token = create_access_token(data)
55+
decoded = validate_token(access_token, "refresh")
56+
assert decoded is None
57+
58+
def test_password_reset_url_generation():
59+
"""
60+
Tests that the password reset URL is correctly formatted and contains
61+
the required query parameters.
62+
"""
63+
test_email = "[email protected]"
64+
test_token = "abc123"
65+
66+
url = generate_password_reset_url(test_email, test_token)
67+
68+
# Parse the URL
69+
parsed = urlparse(url)
70+
query_params = parse_qs(parsed.query)
71+
72+
# Get the actual path from the FastAPI app
73+
reset_password_path: URLPath = app.url_path_for("reset_password")
74+
75+
# Verify URL path
76+
assert parsed.path == str(reset_password_path)
77+
78+
# Verify query parameters
79+
assert "email" in query_params
80+
assert "token" in query_params
81+
assert query_params["email"][0] == test_email
82+
assert query_params["token"][0] == test_token
83+
84+
def test_password_pattern():
85+
"""
86+
Tests that the password pattern is correctly defined. to require at least
87+
one uppercase letter, one lowercase letter, one digit, and one special
88+
character, and at least 8 characters long. Allowed special characters are:
89+
!@#$%^&*()_+-=[]{}|;:,.<>?
90+
"""
91+
special_characters = "!@#$%^&*()_+-=[]{}|;:,.<>?"
92+
uppercase_letters = string.ascii_uppercase
93+
lowercase_letters = string.ascii_lowercase
94+
digits = string.digits
95+
96+
required_elements = {
97+
"special": special_characters,
98+
"uppercase": uppercase_letters,
99+
"lowercase": lowercase_letters,
100+
"digit": digits
101+
}
102+
required_length = 8
103+
104+
# Randomized valid password tests
105+
for _ in range(50):
106+
password = ""
107+
for element in required_elements:
108+
n = random.randint(required_length // len(required_elements), required_length)
109+
password += ''.join(
110+
random.choice(required_elements[element])
111+
for _ in range(n)
112+
)
113+
# Randomize the order of the characters in the string
114+
password = ''.join(random.sample(password, len(password)))
115+
assert re.match(PASSWORD_PATTERN, password) is not None
116+
117+
# Invalid password tests
118+
119+
# Empty password
120+
password = ""
121+
assert re.match(PASSWORD_PATTERN, password) is None
122+
123+
# Too short
124+
password = "aA1!aA1"
125+
assert re.match(PASSWORD_PATTERN, password) is None
126+
127+
# No uppercase letter
128+
password = "a1!" * 3
129+
assert re.match(PASSWORD_PATTERN, password) is None
130+
131+
# No lowercase letter
132+
password = "A1!" * 3
133+
assert re.match(PASSWORD_PATTERN, password) is None
134+
135+
# No digit
136+
password = "aA!" * 3
137+
assert re.match(PASSWORD_PATTERN, password) is None
138+
139+
# No special character
140+
password = "aA1" * 3
141+
assert re.match(PASSWORD_PATTERN, password) is None

tests/test_authentication.py

Lines changed: 0 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,8 @@
1212
from utils.models import User, PasswordResetToken
1313
from utils.auth import (
1414
create_access_token,
15-
create_refresh_token,
1615
verify_password,
17-
get_password_hash,
1816
validate_token,
19-
generate_password_reset_url
2017
)
2118
from .conftest import SetupError
2219

@@ -41,49 +38,6 @@ def mock_resend_send(mock_email_response):
4138
yield mock
4239

4340

44-
# --- Authentication Helper Function Tests ---
45-
46-
47-
def test_password_hashing():
48-
password = "Test123!@#"
49-
hashed = get_password_hash(password)
50-
assert verify_password(password, hashed)
51-
assert not verify_password("wrong_password", hashed)
52-
53-
54-
def test_token_creation_and_validation():
55-
data = {"sub": "[email protected]"}
56-
57-
# Test access token
58-
access_token = create_access_token(data)
59-
decoded = validate_token(access_token, "access")
60-
assert decoded is not None
61-
assert decoded["sub"] == data["sub"]
62-
assert decoded["type"] == "access"
63-
64-
# Test refresh token
65-
refresh_token = create_refresh_token(data)
66-
decoded = validate_token(refresh_token, "refresh")
67-
assert decoded is not None
68-
assert decoded["sub"] == data["sub"]
69-
assert decoded["type"] == "refresh"
70-
71-
72-
def test_expired_token():
73-
data = {"sub": "[email protected]"}
74-
expired_delta = timedelta(minutes=-10)
75-
expired_token = create_access_token(data, expired_delta)
76-
decoded = validate_token(expired_token, "access")
77-
assert decoded is None
78-
79-
80-
def test_invalid_token_type():
81-
data = {"sub": "[email protected]"}
82-
access_token = create_access_token(data)
83-
decoded = validate_token(access_token, "refresh")
84-
assert decoded is None
85-
86-
8741
# --- API Endpoint Tests ---
8842

8943

@@ -272,33 +226,6 @@ def test_password_reset_with_invalid_token(unauth_client: TestClient, test_user:
272226
assert response.status_code == 400
273227

274228

275-
def test_password_reset_url_generation(unauth_client: TestClient):
276-
"""
277-
Tests that the password reset URL is correctly formatted and contains
278-
the required query parameters.
279-
"""
280-
test_email = "[email protected]"
281-
test_token = "abc123"
282-
283-
url = generate_password_reset_url(test_email, test_token)
284-
285-
# Parse the URL
286-
parsed = urlparse(url)
287-
query_params = parse_qs(parsed.query)
288-
289-
# Get the actual path from the FastAPI app
290-
reset_password_path: URLPath = app.url_path_for("reset_password")
291-
292-
# Verify URL path
293-
assert parsed.path == str(reset_password_path)
294-
295-
# Verify query parameters
296-
assert "email" in query_params
297-
assert "token" in query_params
298-
assert query_params["email"][0] == test_email
299-
assert query_params["token"][0] == test_token
300-
301-
302229
def test_password_reset_email_url(unauth_client: TestClient, session: Session, test_user: User, mock_resend_send):
303230
"""
304231
Tests that the password reset email contains a properly formatted reset URL.

utils/auth.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,11 @@
3535
ACCESS_TOKEN_EXPIRE_MINUTES = 30
3636
REFRESH_TOKEN_EXPIRE_DAYS = 30
3737
PASSWORD_PATTERN = re.compile(
38-
r"(?=.*\d)" # At least one digit
39-
r"(?=.*[a-z])" # At least one lowercase letter
40-
r"(?=.*[A-Z])" # At least one uppercase letter
41-
r"(?=.*[@$!%*?&{}<>.,\\'#\-_=+\(\)\[\]:;|~/])" # At least one special character
42-
r"[A-Za-z\d@$!%*?&{}<>.,\\'#\-_=+\(\)\[\]:;|~/]{8,}" # At least 8 characters long
38+
r"(?=.*?\d)" # At least one digit
39+
r"(?=.*?[a-z])" # At least one lowercase letter
40+
r"(?=.*?[A-Z])" # At least one uppercase letter
41+
r"(?=.*?[@$!%*?&{}<>.,\\'#\-_=+\(\)\[\]:;|~/\^])" # At least one special character
42+
r".{8,}" # At least 8 characters long
4343
)
4444

4545

0 commit comments

Comments
 (0)