|
1 | 1 | from unittest.mock import patch |
2 | 2 |
|
3 | 3 | from fastapi.testclient import TestClient |
| 4 | +from pwdlib.hashers.bcrypt import BcryptHasher |
4 | 5 | from sqlmodel import Session |
5 | 6 |
|
6 | 7 | from app.core.config import settings |
7 | | -from app.core.security import verify_password |
| 8 | +from app.core.security import get_password_hash, verify_password |
8 | 9 | from app.crud import create_user |
9 | | -from app.models import UserCreate |
| 10 | +from app.models import User, UserCreate |
10 | 11 | from app.utils import generate_password_reset_token |
11 | 12 | from tests.utils.user import user_authentication_headers |
12 | 13 | from tests.utils.utils import random_email, random_lower_string |
@@ -117,3 +118,68 @@ def test_reset_password_invalid_token( |
117 | 118 | assert "detail" in response |
118 | 119 | assert r.status_code == 400 |
119 | 120 | assert response["detail"] == "Invalid token" |
| 121 | + |
| 122 | + |
| 123 | +def test_login_with_bcrypt_password_upgrades_to_argon2( |
| 124 | + client: TestClient, db: Session |
| 125 | +) -> None: |
| 126 | + """Test that logging in with a bcrypt password hash upgrades it to argon2.""" |
| 127 | + email = random_email() |
| 128 | + password = random_lower_string() |
| 129 | + |
| 130 | + # Create a bcrypt hash directly (simulating legacy password) |
| 131 | + bcrypt_hasher = BcryptHasher() |
| 132 | + bcrypt_hash = bcrypt_hasher.hash(password) |
| 133 | + assert bcrypt_hash.startswith("$2") # bcrypt hashes start with $2 |
| 134 | + |
| 135 | + user = User(email=email, hashed_password=bcrypt_hash, is_active=True) |
| 136 | + db.add(user) |
| 137 | + db.commit() |
| 138 | + db.refresh(user) |
| 139 | + |
| 140 | + assert user.hashed_password.startswith("$2") |
| 141 | + |
| 142 | + login_data = {"username": email, "password": password} |
| 143 | + r = client.post(f"{settings.API_V1_STR}/login/access-token", data=login_data) |
| 144 | + assert r.status_code == 200 |
| 145 | + tokens = r.json() |
| 146 | + assert "access_token" in tokens |
| 147 | + |
| 148 | + db.refresh(user) |
| 149 | + |
| 150 | + # Verify the hash was upgraded to argon2 |
| 151 | + assert user.hashed_password.startswith("$argon2") |
| 152 | + |
| 153 | + verified, updated_hash = verify_password(password, user.hashed_password) |
| 154 | + assert verified |
| 155 | + # Should not need another update since it's already argon2 |
| 156 | + assert updated_hash is None |
| 157 | + |
| 158 | + |
| 159 | +def test_login_with_argon2_password_keeps_hash(client: TestClient, db: Session) -> None: |
| 160 | + """Test that logging in with an argon2 password hash does not update it.""" |
| 161 | + email = random_email() |
| 162 | + password = random_lower_string() |
| 163 | + |
| 164 | + # Create an argon2 hash (current default) |
| 165 | + argon2_hash = get_password_hash(password) |
| 166 | + assert argon2_hash.startswith("$argon2") |
| 167 | + |
| 168 | + # Create user with argon2 hash |
| 169 | + user = User(email=email, hashed_password=argon2_hash, is_active=True) |
| 170 | + db.add(user) |
| 171 | + db.commit() |
| 172 | + db.refresh(user) |
| 173 | + |
| 174 | + original_hash = user.hashed_password |
| 175 | + |
| 176 | + login_data = {"username": email, "password": password} |
| 177 | + r = client.post(f"{settings.API_V1_STR}/login/access-token", data=login_data) |
| 178 | + assert r.status_code == 200 |
| 179 | + tokens = r.json() |
| 180 | + assert "access_token" in tokens |
| 181 | + |
| 182 | + db.refresh(user) |
| 183 | + |
| 184 | + assert user.hashed_password == original_hash |
| 185 | + assert user.hashed_password.startswith("$argon2") |
0 commit comments