11from unittest .mock import patch
22
33from fastapi .testclient import TestClient
4+ from pwdlib .hashers .bcrypt import BcryptHasher
45from sqlmodel import Session
56
67from app .core .config import settings
7- from app .core .security import verify_password
8+ from app .core .security import get_password_hash , verify_password
89from app .crud import create_user
9- from app .models import UserCreate
10+ from app .models import User , UserCreate
1011from app .utils import generate_password_reset_token
1112from tests .utils .user import user_authentication_headers
1213from tests .utils .utils import random_email , random_lower_string
@@ -99,7 +100,8 @@ def test_reset_password(client: TestClient, db: Session) -> None:
99100 assert r .json () == {"message" : "Password updated successfully" }
100101
101102 db .refresh (user )
102- assert verify_password (new_password , user .hashed_password )
103+ verified , _ = verify_password (new_password , user .hashed_password )
104+ assert verified
103105
104106
105107def test_reset_password_invalid_token (
@@ -116,3 +118,68 @@ def test_reset_password_invalid_token(
116118 assert "detail" in response
117119 assert r .status_code == 400
118120 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