← full-stack-fastapi-template / backend/tests/crud/test_user.py
| 1 | from fastapi.encoders import jsonable_encoder |
| 2 | from pwdlib.hashers.bcrypt import BcryptHasher |
| 3 | from sqlmodel import Session |
| 4 | |
| 5 | from app import crud |
| 6 | from app.core.security import verify_password |
| 7 | from app.models import User, UserCreate, UserUpdate |
| 8 | from tests.utils.utils import random_email, random_lower_string |
| 9 | |
| 10 | |
| 11 | def test_create_user(db: Session) -> None: |
| 12 | email = random_email() |
| 13 | password = random_lower_string() |
| 14 | user_in = UserCreate(email=email, password=password) |
| 15 | user = crud.create_user(session=db, user_create=user_in) |
| 16 | assert user.email == email |
| 17 | assert hasattr(user, "hashed_password") |
| 18 | |
| 19 | |
| 20 | def test_authenticate_user(db: Session) -> None: |
| 21 | email = random_email() |
| 22 | password = random_lower_string() |
| 23 | user_in = UserCreate(email=email, password=password) |
| 24 | user = crud.create_user(session=db, user_create=user_in) |
| 25 | authenticated_user = crud.authenticate(session=db, email=email, password=password) |
| 26 | assert authenticated_user |
| 27 | assert user.email == authenticated_user.email |
| 28 | |
| 29 | |
| 30 | def test_not_authenticate_user(db: Session) -> None: |
| 31 | email = random_email() |
| 32 | password = random_lower_string() |
| 33 | user = crud.authenticate(session=db, email=email, password=password) |
| 34 | assert user is None |
| 35 | |
| 36 | |
| 37 | def test_check_if_user_is_active(db: Session) -> None: |
| 38 | email = random_email() |
| 39 | password = random_lower_string() |
| 40 | user_in = UserCreate(email=email, password=password) |
| 41 | user = crud.create_user(session=db, user_create=user_in) |
| 42 | assert user.is_active is True |
| 43 | |
| 44 | |
| 45 | def test_check_if_user_is_active_inactive(db: Session) -> None: |
| 46 | email = random_email() |
| 47 | password = random_lower_string() |
| 48 | user_in = UserCreate(email=email, password=password, is_active=False) |
| 49 | user = crud.create_user(session=db, user_create=user_in) |
| 50 | assert user.is_active is False |
| 51 | |
| 52 | |
| 53 | def test_check_if_user_is_superuser(db: Session) -> None: |
| 54 | email = random_email() |
| 55 | password = random_lower_string() |
| 56 | user_in = UserCreate(email=email, password=password, is_superuser=True) |
| 57 | user = crud.create_user(session=db, user_create=user_in) |
| 58 | assert user.is_superuser is True |
| 59 | |
| 60 | |
| 61 | def test_check_if_user_is_superuser_normal_user(db: Session) -> None: |
| 62 | username = random_email() |
| 63 | password = random_lower_string() |
| 64 | user_in = UserCreate(email=username, password=password) |
| 65 | user = crud.create_user(session=db, user_create=user_in) |
| 66 | assert user.is_superuser is False |
| 67 | |
| 68 | |
| 69 | def test_get_user(db: Session) -> None: |
| 70 | password = random_lower_string() |
| 71 | username = random_email() |
| 72 | user_in = UserCreate(email=username, password=password, is_superuser=True) |
| 73 | user = crud.create_user(session=db, user_create=user_in) |
| 74 | user_2 = db.get(User, user.id) |
| 75 | assert user_2 |
| 76 | assert user.email == user_2.email |
| 77 | assert jsonable_encoder(user) == jsonable_encoder(user_2) |
| 78 | |
| 79 | |
| 80 | def test_update_user(db: Session) -> None: |
| 81 | password = random_lower_string() |
| 82 | email = random_email() |
| 83 | user_in = UserCreate(email=email, password=password, is_superuser=True) |
| 84 | user = crud.create_user(session=db, user_create=user_in) |
| 85 | new_password = random_lower_string() |
| 86 | user_in_update = UserUpdate(password=new_password, is_superuser=True) |
| 87 | if user.id is not None: |
| 88 | crud.update_user(session=db, db_user=user, user_in=user_in_update) |
| 89 | user_2 = db.get(User, user.id) |
| 90 | assert user_2 |
| 91 | assert user.email == user_2.email |
| 92 | verified, _ = verify_password(new_password, user_2.hashed_password) |
| 93 | assert verified |
| 94 | |
| 95 | |
| 96 | def test_authenticate_user_with_bcrypt_upgrades_to_argon2(db: Session) -> None: |
| 97 | """Test that a user with bcrypt password hash gets upgraded to argon2 on login.""" |
| 98 | email = random_email() |
| 99 | password = random_lower_string() |
| 100 | |
| 101 | # Create a bcrypt hash directly (simulating legacy password) |
| 102 | bcrypt_hasher = BcryptHasher() |
| 103 | bcrypt_hash = bcrypt_hasher.hash(password) |
| 104 | assert bcrypt_hash.startswith("$2") # bcrypt hashes start with $2 |
| 105 | |
| 106 | # Create user with bcrypt hash directly in the database |
| 107 | user = User(email=email, hashed_password=bcrypt_hash) |
| 108 | db.add(user) |
| 109 | db.commit() |
| 110 | db.refresh(user) |
| 111 | |
| 112 | # Verify the hash is bcrypt before authentication |
| 113 | assert user.hashed_password.startswith("$2") |
| 114 | |
| 115 | # Authenticate - this should upgrade the hash to argon2 |
| 116 | authenticated_user = crud.authenticate(session=db, email=email, password=password) |
| 117 | assert authenticated_user |
| 118 | assert authenticated_user.email == email |
| 119 | |
| 120 | db.refresh(authenticated_user) |
| 121 | |
| 122 | # Verify the hash was upgraded to argon2 |
| 123 | assert authenticated_user.hashed_password.startswith("$argon2") |
| 124 | |
| 125 | verified, updated_hash = verify_password( |
| 126 | password, authenticated_user.hashed_password |
| 127 | ) |
| 128 | assert verified |
| 129 | # Should not need another update since it's already argon2 |
| 130 | assert updated_hash is None |
| 131 |