Add tests for authentication and utilities, and update dependencies
- Created a new test package for services and added tests for AuthService. - Implemented tests for user registration, login, and token creation. - Added a new test package for utilities and included tests for password and JWT utilities. - Updated `uv.lock` to include new dependencies: bcrypt, email-validator, pyjwt, and pytest-asyncio.
This commit is contained in:
1
tests/utils/__init__.py
Normal file
1
tests/utils/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Utils tests package."""
|
||||
182
tests/utils/test_auth_utils.py
Normal file
182
tests/utils/test_auth_utils.py
Normal file
@@ -0,0 +1,182 @@
|
||||
"""Tests for authentication utilities."""
|
||||
|
||||
from datetime import UTC, datetime, timedelta
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
from app.utils.auth import JWTUtils, PasswordUtils, TokenUtils
|
||||
|
||||
PASSWORD_HASH_LENGTH_GT = 50
|
||||
TOKEN_LENGTH_GT = 20
|
||||
TOKEN_DOTS_COUNT = 2
|
||||
STATUS_CODE_UNAUTHORIZED = 401
|
||||
|
||||
|
||||
class TestPasswordUtils:
|
||||
"""Test password utility functions."""
|
||||
|
||||
def test_hash_password(self) -> None:
|
||||
"""Test password hashing."""
|
||||
password = "testpassword123"
|
||||
hashed = PasswordUtils.hash_password(password)
|
||||
|
||||
# Hash should be different from original password
|
||||
assert hashed != password
|
||||
# Hash should be a string
|
||||
assert isinstance(hashed, str)
|
||||
# Hash should have reasonable length (bcrypt produces ~60 chars)
|
||||
assert len(hashed) > PASSWORD_HASH_LENGTH_GT
|
||||
|
||||
def test_hash_password_different_salts(self) -> None:
|
||||
"""Test that same password produces different hashes (different salts)."""
|
||||
password = "testpassword123"
|
||||
hash1 = PasswordUtils.hash_password(password)
|
||||
hash2 = PasswordUtils.hash_password(password)
|
||||
|
||||
# Same password should produce different hashes due to different salts
|
||||
assert hash1 != hash2
|
||||
|
||||
def test_verify_password_correct(self) -> None:
|
||||
"""Test password verification with correct password."""
|
||||
password = "testpassword123"
|
||||
hashed = PasswordUtils.hash_password(password)
|
||||
|
||||
# Correct password should verify
|
||||
assert PasswordUtils.verify_password(password, hashed) is True
|
||||
|
||||
def test_verify_password_incorrect(self) -> None:
|
||||
"""Test password verification with incorrect password."""
|
||||
password = "testpassword123"
|
||||
wrong_password = "wrongpassword"
|
||||
hashed = PasswordUtils.hash_password(password)
|
||||
|
||||
# Wrong password should not verify
|
||||
assert PasswordUtils.verify_password(wrong_password, hashed) is False
|
||||
|
||||
def test_verify_password_empty(self) -> None:
|
||||
"""Test password verification with empty password."""
|
||||
password = "testpassword123"
|
||||
hashed = PasswordUtils.hash_password(password)
|
||||
|
||||
# Empty password should not verify
|
||||
assert PasswordUtils.verify_password("", hashed) is False
|
||||
|
||||
|
||||
class TestJWTUtils:
|
||||
"""Test JWT utility functions."""
|
||||
|
||||
def test_create_access_token(self) -> None:
|
||||
"""Test JWT token creation."""
|
||||
data = {"sub": "123", "email": "test@example.com"}
|
||||
token = JWTUtils.create_access_token(data)
|
||||
|
||||
# Token should be a string
|
||||
assert isinstance(token, str)
|
||||
# Token should have reasonable length
|
||||
assert len(token) > PASSWORD_HASH_LENGTH_GT
|
||||
# Token should contain dots (JWT format)
|
||||
assert token.count(".") == TOKEN_DOTS_COUNT
|
||||
|
||||
def test_create_access_token_with_expiry(self) -> None:
|
||||
"""Test JWT token creation with custom expiry."""
|
||||
data = {"sub": "123", "email": "test@example.com"}
|
||||
expires_delta = timedelta(minutes=5)
|
||||
token = JWTUtils.create_access_token(data, expires_delta)
|
||||
|
||||
# Should create a valid token
|
||||
assert isinstance(token, str)
|
||||
assert len(token) > PASSWORD_HASH_LENGTH_GT
|
||||
|
||||
def test_decode_access_token(self) -> None:
|
||||
"""Test JWT token decoding."""
|
||||
original_data = {"sub": "123", "email": "test@example.com", "role": "user"}
|
||||
token = JWTUtils.create_access_token(original_data)
|
||||
|
||||
decoded_data = JWTUtils.decode_access_token(token)
|
||||
|
||||
# Should decode to original data (plus exp)
|
||||
assert decoded_data["sub"] == original_data["sub"]
|
||||
assert decoded_data["email"] == original_data["email"]
|
||||
assert decoded_data["role"] == original_data["role"]
|
||||
assert "exp" in decoded_data
|
||||
|
||||
def test_decode_invalid_token(self) -> None:
|
||||
"""Test decoding invalid JWT token."""
|
||||
invalid_token = "invalid.token.here"
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
JWTUtils.decode_access_token(invalid_token)
|
||||
|
||||
assert exc_info.value.status_code == STATUS_CODE_UNAUTHORIZED
|
||||
assert "Could not validate credentials" in exc_info.value.detail
|
||||
|
||||
def test_decode_expired_token(self) -> None:
|
||||
"""Test decoding expired JWT token."""
|
||||
data = {"sub": "123", "email": "test@example.com"}
|
||||
# Create token that expires immediately
|
||||
expires_delta = timedelta(seconds=-1)
|
||||
token = JWTUtils.create_access_token(data, expires_delta)
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
JWTUtils.decode_access_token(token)
|
||||
|
||||
assert exc_info.value.status_code == STATUS_CODE_UNAUTHORIZED
|
||||
assert "Token has expired" in exc_info.value.detail
|
||||
|
||||
def test_decode_empty_token(self) -> None:
|
||||
"""Test decoding empty token."""
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
JWTUtils.decode_access_token("")
|
||||
|
||||
assert exc_info.value.status_code == STATUS_CODE_UNAUTHORIZED
|
||||
|
||||
|
||||
class TestTokenUtils:
|
||||
"""Test token utility functions."""
|
||||
|
||||
def test_generate_api_token(self) -> None:
|
||||
"""Test API token generation."""
|
||||
token = TokenUtils.generate_api_token()
|
||||
|
||||
# Token should be a string
|
||||
assert isinstance(token, str)
|
||||
# Token should have reasonable length
|
||||
assert len(token) > TOKEN_LENGTH_GT
|
||||
# Token should be URL-safe
|
||||
assert all(c.isalnum() or c in "-_" for c in token)
|
||||
|
||||
def test_generate_api_token_unique(self) -> None:
|
||||
"""Test that API tokens are unique."""
|
||||
token1 = TokenUtils.generate_api_token()
|
||||
token2 = TokenUtils.generate_api_token()
|
||||
|
||||
# Tokens should be different
|
||||
assert token1 != token2
|
||||
|
||||
def test_is_token_expired_none(self) -> None:
|
||||
"""Test token expiry check with None."""
|
||||
# None expiry should not be expired
|
||||
assert TokenUtils.is_token_expired(None) is False
|
||||
|
||||
def test_is_token_expired_future(self) -> None:
|
||||
"""Test token expiry check with future date."""
|
||||
future_date = datetime.now(UTC) + timedelta(hours=1)
|
||||
|
||||
# Future date should not be expired
|
||||
assert TokenUtils.is_token_expired(future_date) is False
|
||||
|
||||
def test_is_token_expired_past(self) -> None:
|
||||
"""Test token expiry check with past date."""
|
||||
past_date = datetime.now(UTC) - timedelta(hours=1)
|
||||
|
||||
# Past date should be expired
|
||||
assert TokenUtils.is_token_expired(past_date) is True
|
||||
|
||||
def test_is_token_expired_naive_datetime(self) -> None:
|
||||
"""Test token expiry check with naive datetime."""
|
||||
# Create a past naive datetime (using UTC time to be consistent)
|
||||
past_date = datetime.now(UTC) - timedelta(hours=1)
|
||||
|
||||
# Should handle naive datetime (treat as UTC)
|
||||
assert TokenUtils.is_token_expired(past_date) is True
|
||||
Reference in New Issue
Block a user