Skip to content

Commit e209258

Browse files
Added a helper function to generate the reset URL and added a test to check it against the corresponding API endpoint
1 parent 074f219 commit e209258

File tree

2 files changed

+87
-3
lines changed

2 files changed

+87
-3
lines changed

tests/test_authentication.py

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import pytest
22
from fastapi.testclient import TestClient
3+
from starlette.datastructures import URLPath
34
from sqlmodel import Session, select
45
from datetime import timedelta
56
from unittest.mock import patch
67
import resend
8+
from urllib.parse import urlparse, parse_qs
79

810
from main import app
911
from utils.models import User, PasswordResetToken
@@ -13,7 +15,8 @@
1315
create_refresh_token,
1416
verify_password,
1517
get_password_hash,
16-
validate_token
18+
validate_token,
19+
generate_password_reset_url
1720
)
1821

1922

@@ -292,3 +295,69 @@ def test_password_reset_with_invalid_token(client: TestClient, test_user: User):
292295
}
293296
)
294297
assert response.status_code == 400
298+
299+
300+
def test_password_reset_url_generation(client: TestClient):
301+
"""
302+
Tests that the password reset URL is correctly formatted and contains
303+
the required query parameters.
304+
"""
305+
test_email = "[email protected]"
306+
test_token = "abc123"
307+
308+
url = generate_password_reset_url(test_email, test_token)
309+
310+
# Parse the URL
311+
parsed = urlparse(url)
312+
query_params = parse_qs(parsed.query)
313+
314+
# Get the actual path from the FastAPI app
315+
reset_password_path: URLPath = app.url_path_for("reset_password")
316+
317+
# Verify URL path
318+
assert parsed.path == str(reset_password_path)
319+
320+
# Verify query parameters
321+
assert "email" in query_params
322+
assert "token" in query_params
323+
assert query_params["email"][0] == test_email
324+
assert query_params["token"][0] == test_token
325+
326+
327+
def test_password_reset_email_url(client: TestClient, session: Session, test_user: User, mock_resend_send):
328+
"""
329+
Tests that the password reset email contains a properly formatted reset URL.
330+
"""
331+
response = client.post(
332+
"/auth/forgot_password",
333+
data={"email": test_user.email},
334+
follow_redirects=False
335+
)
336+
assert response.status_code == 303
337+
338+
# Get the reset token from the database
339+
reset_token = session.exec(select(PasswordResetToken)
340+
.where(PasswordResetToken.user_id == test_user.id)).first()
341+
assert reset_token is not None
342+
343+
# Get the actual path from the FastAPI app
344+
reset_password_path: URLPath = app.url_path_for("reset_password")
345+
346+
# Verify the email HTML contains the correct URL
347+
mock_resend_send.assert_called_once()
348+
call_args = mock_resend_send.call_args[0][0]
349+
html_content = call_args["html"]
350+
351+
# Extract URL from HTML
352+
import re
353+
url_match = re.search(r'href=[\'"]([^\'"]*)[\'"]', html_content)
354+
assert url_match is not None
355+
reset_url = url_match.group(1)
356+
357+
# Parse and verify the URL
358+
parsed = urlparse(reset_url)
359+
query_params = parse_qs(parsed.query)
360+
361+
assert parsed.path == str(reset_password_path)
362+
assert query_params["email"][0] == test_user.email
363+
assert query_params["token"][0] == reset_token.token

utils/auth.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,21 @@ def __init__(self, user: User, access_token: str, refresh_token: str):
258258
self.refresh_token = refresh_token
259259

260260

261+
def generate_password_reset_url(email: str, token: str) -> str:
262+
"""
263+
Generates the password reset URL with proper query parameters.
264+
265+
Args:
266+
email: User's email address
267+
token: Password reset token
268+
269+
Returns:
270+
Complete password reset URL
271+
"""
272+
base_url = os.getenv('BASE_URL')
273+
return f"{base_url}/auth/reset_password?email={email}&token={token}"
274+
275+
261276
def send_reset_email(email: str, session: Session):
262277
# Check for an existing unexpired token
263278
user = session.exec(select(User).where(User.email == email)).first()
@@ -281,12 +296,12 @@ def send_reset_email(email: str, session: Session):
281296
session.add(reset_token)
282297

283298
try:
284-
# TODO: Use a templating engine
299+
reset_url = generate_password_reset_url(email, token)
285300
params: resend.Emails.SendParams = {
286301
"from": "[email protected]",
287302
"to": [email],
288303
"subject": "Password Reset Request",
289-
"html": f"<p>Click <a href='{os.getenv('BASE_URL')}/auth/reset_password?email={email}&token={token}'>here</a> to reset your password.</p>",
304+
"html": f"<p>Click <a href='{reset_url}'>here</a> to reset your password.</p>",
290305
}
291306

292307
sent_email: resend.Email = resend.Emails.send(params)

0 commit comments

Comments
 (0)