""" Unit tests for authentication system (app/auth.py) Tests JWT tokens, user management, and password hashing with SQLModel support """ import pytest from datetime import datetime, timedelta from unittest.mock import patch from app.auth import UserManager, create_access_token, verify_token, get_user_from_token from app.models.auth import UserTable class TestUserManager: """Tests for UserManager class using SQLModel""" def test_create_user_success(self, user_manager): """Test successful user creation""" user = user_manager.create_user("testuser", "password123") assert user.username == "testuser" assert hasattr(user, "hashed_password") assert user.created_at is not None assert user.last_login is None # Verify it's in the database db_user = user_manager.get_user("testuser") assert db_user is not None assert db_user.username == "testuser" def test_create_user_hashing(self, user_manager): """Test that passwords are properly hashed with bcrypt""" user = user_manager.create_user("testuser", "password123") # Hash should not be the plain password assert user.hashed_password != "password123" # Bcrypt hashes start with $2b$ assert user.hashed_password.startswith("$2b$") # Hash should be 60 characters (bcrypt standard) assert len(user.hashed_password) == 60 def test_create_user_duplicate(self, user_manager): """Test that duplicate usernames are rejected""" user_manager.create_user("testuser", "password123") with pytest.raises(ValueError, match="already exists"): user_manager.create_user("testuser", "different456") def test_create_user_password_truncation(self, user_manager): """Test that passwords longer than 72 bytes are truncated (bcrypt limit)""" long_password = "a" * 100 user = user_manager.create_user("testuser", long_password) assert user.username == "testuser" def test_authenticate_user_success(self, user_manager): """Test successful user authentication""" user_manager.create_user("testuser", "password123") user = user_manager.authenticate_user("testuser", "password123") assert user is not None assert user.username == "testuser" assert user.last_login is not None def test_authenticate_user_wrong_password(self, user_manager): """Test authentication with wrong password""" user_manager.create_user("testuser", "password123") user = user_manager.authenticate_user("testuser", "wrongpassword") assert user is None def test_authenticate_user_nonexistent(self, user_manager): """Test authentication with non-existent user""" user = user_manager.authenticate_user("nonexistent", "password") assert user is None def test_authenticate_updates_last_login(self, user_manager): """Test that authentication updates last_login timestamp""" user_manager.create_user("testuser", "password123") user_before = user_manager.get_user("testuser") assert user_before.last_login is None user_manager.authenticate_user("testuser", "password123") user_after = user_manager.get_user("testuser") assert user_after.last_login is not None def test_get_user(self, user_manager): """Test getting a user by username""" user_manager.create_user("testuser", "password123") user = user_manager.get_user("testuser") assert user is not None assert user.username == "testuser" def test_get_user_by_id(self, user_manager): """Test getting a user by ID""" user = user_manager.create_user("testuser", "password123") user_id = user.id db_user = user_manager.get_user_by_id(user_id) assert db_user is not None assert db_user.username == "testuser" def test_get_user_nonexistent(self, user_manager): """Test getting a non-existent user""" user = user_manager.get_user("nonexistent") assert user is None def test_update_user(self, user_manager): """Test updating user information""" user = user_manager.create_user("testuser", "password123") updated = user_manager.update_user(user.id, {"full_name": "New Name", "email": "new@example.com"}) assert updated.full_name == "New Name" assert updated.email == "new@example.com" db_user = user_manager.get_user("testuser") assert db_user.full_name == "New Name" class TestJWTToken: """Tests for JWT token functions""" def test_create_access_token(self): """Test creating an access token""" token = create_access_token({"sub": "testuser"}) assert isinstance(token, str) assert len(token) > 0 def test_verify_token_success(self): """Test verifying a valid token""" token = create_access_token({"sub": "testuser"}) username = verify_token(token) assert username == "testuser" @pytest.mark.skip(reason="Problematic mock with datetime and jose library") def test_verify_token_expired(self): """Test verifying an expired token""" with patch('app.auth.datetime') as mock_datetime: # Set fixed time now = datetime.utcnow() mock_datetime.utcnow.return_value = now # Create token that expires in 1 minute token = create_access_token({"sub": "testuser"}, expires_delta=timedelta(minutes=1)) # Move time forward by 2 minutes mock_datetime.utcnow.return_value = now + timedelta(minutes=2) username = verify_token(token) assert username is None def test_verify_token_invalid(self): """Test verifying an invalid token""" username = verify_token("invalid-token") assert username is None def test_get_user_from_token(self): """Test get_user_from_token alias""" token = create_access_token({"sub": "testuser"}) username = get_user_from_token(token) assert username == "testuser"