|
1 | 1 | import pytest
|
2 | 2 | from fastapi.testclient import TestClient
|
| 3 | +from starlette.datastructures import URLPath |
3 | 4 | from sqlmodel import Session, select
|
4 | 5 | from datetime import timedelta
|
5 | 6 | from unittest.mock import patch
|
6 | 7 | import resend
|
| 8 | +from urllib.parse import urlparse, parse_qs |
7 | 9 |
|
8 | 10 | from main import app
|
9 | 11 | from utils.models import User, PasswordResetToken
|
|
13 | 15 | create_refresh_token,
|
14 | 16 | verify_password,
|
15 | 17 | get_password_hash,
|
16 |
| - validate_token |
| 18 | + validate_token, |
| 19 | + generate_password_reset_url |
17 | 20 | )
|
18 | 21 |
|
19 | 22 |
|
@@ -292,3 +295,69 @@ def test_password_reset_with_invalid_token(client: TestClient, test_user: User):
|
292 | 295 | }
|
293 | 296 | )
|
294 | 297 | 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 | + |
| 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 |
0 commit comments