- 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.
183 lines
6.7 KiB
Python
183 lines
6.7 KiB
Python
"""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
|