375 lines
13 KiB
Python
375 lines
13 KiB
Python
"""
|
|
Unit tests for authentication endpoints
|
|
"""
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
from unittest.mock import patch
|
|
import uuid
|
|
|
|
from src.backend.main import app
|
|
from src.backend.models.user import User
|
|
from src.backend.api.auth import hash_password, verify_password, create_access_token
|
|
|
|
class TestAuthenticationAPI:
|
|
"""Test authentication API endpoints."""
|
|
|
|
def test_register_user_success(self, test_client, test_db):
|
|
"""Test successful user registration."""
|
|
user_data = {
|
|
"email": "newuser@test.com",
|
|
"password": "securepassword123",
|
|
"first_name": "New",
|
|
"last_name": "User"
|
|
}
|
|
|
|
response = test_client.post("/api/auth/register", json=user_data)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["email"] == "newuser@test.com"
|
|
assert data["full_name"] == "New User"
|
|
assert data["first_name"] == "New"
|
|
assert data["last_name"] == "User"
|
|
assert data["is_active"] == True
|
|
assert "id" in data
|
|
# Password should never be returned
|
|
assert "password" not in data
|
|
assert "password_hash" not in data
|
|
|
|
def test_register_user_duplicate_email(self, test_client, test_user):
|
|
"""Test registration with duplicate email fails."""
|
|
user_data = {
|
|
"email": test_user.email, # Use existing user's email
|
|
"password": "differentpassword",
|
|
"first_name": "Duplicate",
|
|
"last_name": "User"
|
|
}
|
|
|
|
response = test_client.post("/api/auth/register", json=user_data)
|
|
|
|
assert response.status_code == 400
|
|
data = response.json()
|
|
assert "already registered" in data["detail"].lower()
|
|
|
|
def test_register_user_invalid_email(self, test_client):
|
|
"""Test registration with invalid email format."""
|
|
user_data = {
|
|
"email": "invalid-email-format",
|
|
"password": "securepassword123",
|
|
"first_name": "Invalid",
|
|
"last_name": "Email"
|
|
}
|
|
|
|
response = test_client.post("/api/auth/register", json=user_data)
|
|
|
|
assert response.status_code == 422 # Validation error
|
|
|
|
def test_register_user_missing_fields(self, test_client):
|
|
"""Test registration with missing required fields."""
|
|
user_data = {
|
|
"email": "incomplete@test.com",
|
|
# Missing password, first_name, last_name
|
|
}
|
|
|
|
response = test_client.post("/api/auth/register", json=user_data)
|
|
|
|
assert response.status_code == 422
|
|
|
|
def test_login_success(self, test_client, test_user):
|
|
"""Test successful login."""
|
|
login_data = {
|
|
"email": test_user.email,
|
|
"password": "testpassword123"
|
|
}
|
|
|
|
response = test_client.post("/api/auth/login", json=login_data)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "access_token" in data
|
|
assert "token_type" in data
|
|
assert data["token_type"] == "bearer"
|
|
|
|
# Verify token structure
|
|
token = data["access_token"]
|
|
assert len(token.split('.')) == 3 # JWT has 3 parts
|
|
|
|
def test_login_wrong_password(self, test_client, test_user):
|
|
"""Test login with incorrect password."""
|
|
login_data = {
|
|
"email": test_user.email,
|
|
"password": "wrongpassword"
|
|
}
|
|
|
|
response = test_client.post("/api/auth/login", json=login_data)
|
|
|
|
assert response.status_code == 401
|
|
data = response.json()
|
|
assert "incorrect" in data["detail"].lower()
|
|
|
|
def test_login_nonexistent_user(self, test_client):
|
|
"""Test login with non-existent user."""
|
|
login_data = {
|
|
"email": "nonexistent@test.com",
|
|
"password": "somepassword"
|
|
}
|
|
|
|
response = test_client.post("/api/auth/login", json=login_data)
|
|
|
|
assert response.status_code == 401
|
|
|
|
def test_login_invalid_email_format(self, test_client):
|
|
"""Test login with invalid email format."""
|
|
login_data = {
|
|
"email": "not-an-email",
|
|
"password": "somepassword"
|
|
}
|
|
|
|
response = test_client.post("/api/auth/login", json=login_data)
|
|
|
|
assert response.status_code == 422 # Validation error
|
|
|
|
def test_get_current_user_success(self, test_client, test_user_token):
|
|
"""Test getting current user with valid token."""
|
|
headers = {"Authorization": f"Bearer {test_user_token}"}
|
|
response = test_client.get("/api/auth/me", headers=headers)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "email" in data
|
|
assert "id" in data
|
|
assert "full_name" in data
|
|
assert "is_active" in data
|
|
# Ensure sensitive data is not returned
|
|
assert "password" not in data
|
|
assert "password_hash" not in data
|
|
|
|
def test_get_current_user_invalid_token(self, test_client):
|
|
"""Test getting current user with invalid token."""
|
|
headers = {"Authorization": "Bearer invalid.token.here"}
|
|
response = test_client.get("/api/auth/me", headers=headers)
|
|
|
|
assert response.status_code == 401
|
|
|
|
def test_get_current_user_no_token(self, test_client):
|
|
"""Test getting current user without authorization header."""
|
|
response = test_client.get("/api/auth/me")
|
|
|
|
assert response.status_code == 403 # FastAPI HTTPBearer returns 403
|
|
|
|
def test_get_current_user_malformed_header(self, test_client):
|
|
"""Test getting current user with malformed authorization header."""
|
|
malformed_headers = [
|
|
{"Authorization": "Bearer"},
|
|
{"Authorization": "NotBearer validtoken"},
|
|
{"Authorization": "Bearer "},
|
|
{"Authorization": "invalid-format"}
|
|
]
|
|
|
|
for headers in malformed_headers:
|
|
response = test_client.get("/api/auth/me", headers=headers)
|
|
assert response.status_code in [401, 403]
|
|
|
|
|
|
class TestPasswordUtilities:
|
|
"""Test password hashing and verification utilities."""
|
|
|
|
def test_hash_password(self):
|
|
"""Test password hashing function."""
|
|
password = "testpassword123"
|
|
hashed = hash_password(password)
|
|
|
|
assert hashed != password # Should be hashed
|
|
assert len(hashed) > 0
|
|
assert hashed.startswith('$2b$') # bcrypt format
|
|
|
|
def test_verify_password_correct(self):
|
|
"""Test password verification with correct password."""
|
|
password = "testpassword123"
|
|
hashed = hash_password(password)
|
|
|
|
assert verify_password(password, hashed) == True
|
|
|
|
def test_verify_password_incorrect(self):
|
|
"""Test password verification with incorrect password."""
|
|
password = "testpassword123"
|
|
wrong_password = "wrongpassword"
|
|
hashed = hash_password(password)
|
|
|
|
assert verify_password(wrong_password, hashed) == False
|
|
|
|
def test_hash_different_passwords_different_hashes(self):
|
|
"""Test that different passwords produce different hashes."""
|
|
password1 = "password123"
|
|
password2 = "password456"
|
|
|
|
hash1 = hash_password(password1)
|
|
hash2 = hash_password(password2)
|
|
|
|
assert hash1 != hash2
|
|
|
|
def test_hash_same_password_different_hashes(self):
|
|
"""Test that same password produces different hashes (salt)."""
|
|
password = "testpassword123"
|
|
|
|
hash1 = hash_password(password)
|
|
hash2 = hash_password(password)
|
|
|
|
assert hash1 != hash2 # Should be different due to salt
|
|
# But both should verify correctly
|
|
assert verify_password(password, hash1) == True
|
|
assert verify_password(password, hash2) == True
|
|
|
|
|
|
class TestJWTTokens:
|
|
"""Test JWT token creation and validation."""
|
|
|
|
def test_create_access_token(self):
|
|
"""Test JWT token creation."""
|
|
data = {"sub": str(uuid.uuid4()), "email": "test@example.com"}
|
|
token = create_access_token(data)
|
|
|
|
assert isinstance(token, str)
|
|
assert len(token.split('.')) == 3 # JWT format: header.payload.signature
|
|
|
|
def test_create_token_with_different_data(self):
|
|
"""Test that different data creates different tokens."""
|
|
data1 = {"sub": str(uuid.uuid4()), "email": "user1@example.com"}
|
|
data2 = {"sub": str(uuid.uuid4()), "email": "user2@example.com"}
|
|
|
|
token1 = create_access_token(data1)
|
|
token2 = create_access_token(data2)
|
|
|
|
assert token1 != token2
|
|
|
|
def test_token_contains_expiration(self):
|
|
"""Test that created tokens contain expiration claim."""
|
|
from jose import jwt
|
|
from src.backend.core.config import settings
|
|
|
|
data = {"sub": str(uuid.uuid4())}
|
|
token = create_access_token(data)
|
|
|
|
# Decode without verification to check claims
|
|
decoded = jwt.get_unverified_claims(token)
|
|
assert "exp" in decoded
|
|
assert "sub" in decoded
|
|
|
|
|
|
class TestUserModel:
|
|
"""Test User model properties and methods."""
|
|
|
|
def test_user_full_name_property(self):
|
|
"""Test that full_name property works correctly."""
|
|
user = User(
|
|
email="test@example.com",
|
|
password_hash="hashed_password",
|
|
full_name="John Doe"
|
|
)
|
|
|
|
assert user.full_name == "John Doe"
|
|
assert user.first_name == "John"
|
|
assert user.last_name == "Doe"
|
|
|
|
def test_user_single_name(self):
|
|
"""Test user with single name."""
|
|
user = User(
|
|
email="test@example.com",
|
|
password_hash="hashed_password",
|
|
full_name="Madonna"
|
|
)
|
|
|
|
assert user.full_name == "Madonna"
|
|
assert user.first_name == "Madonna"
|
|
assert user.last_name == ""
|
|
|
|
def test_user_multiple_last_names(self):
|
|
"""Test user with multiple last names."""
|
|
user = User(
|
|
email="test@example.com",
|
|
password_hash="hashed_password",
|
|
full_name="John van der Berg"
|
|
)
|
|
|
|
assert user.full_name == "John van der Berg"
|
|
assert user.first_name == "John"
|
|
assert user.last_name == "van der Berg"
|
|
|
|
def test_user_is_active_property(self):
|
|
"""Test user is_active property."""
|
|
user = User(
|
|
email="test@example.com",
|
|
password_hash="hashed_password",
|
|
full_name="Test User"
|
|
)
|
|
|
|
assert user.is_active == True # Default is True
|
|
|
|
|
|
class TestAuthenticationEdgeCases:
|
|
"""Test edge cases and error conditions."""
|
|
|
|
def test_register_empty_names(self, test_client):
|
|
"""Test registration with empty names."""
|
|
user_data = {
|
|
"email": "empty@test.com",
|
|
"password": "password123",
|
|
"first_name": "",
|
|
"last_name": ""
|
|
}
|
|
|
|
response = test_client.post("/api/auth/register", json=user_data)
|
|
|
|
# Should still work but create empty full_name
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["full_name"] == " " # Space between empty names
|
|
|
|
def test_register_very_long_email(self, test_client):
|
|
"""Test registration with very long email."""
|
|
long_email = "a" * 250 + "@test.com" # Very long email
|
|
user_data = {
|
|
"email": long_email,
|
|
"password": "password123",
|
|
"first_name": "Long",
|
|
"last_name": "Email"
|
|
}
|
|
|
|
response = test_client.post("/api/auth/register", json=user_data)
|
|
|
|
# Should handle long emails (within DB constraints)
|
|
if len(long_email) <= 255:
|
|
assert response.status_code == 200
|
|
else:
|
|
assert response.status_code in [400, 422]
|
|
|
|
def test_register_unicode_names(self, test_client):
|
|
"""Test registration with unicode characters in names."""
|
|
user_data = {
|
|
"email": "unicode@test.com",
|
|
"password": "password123",
|
|
"first_name": "José",
|
|
"last_name": "González"
|
|
}
|
|
|
|
response = test_client.post("/api/auth/register", json=user_data)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["full_name"] == "José González"
|
|
assert data["first_name"] == "José"
|
|
assert data["last_name"] == "González"
|
|
|
|
def test_case_insensitive_email_login(self, test_client, test_user):
|
|
"""Test that email login is case insensitive."""
|
|
# Try login with different case
|
|
login_data = {
|
|
"email": test_user.email.upper(),
|
|
"password": "testpassword123"
|
|
}
|
|
|
|
response = test_client.post("/api/auth/login", json=login_data)
|
|
|
|
# This might fail if email comparison is case-sensitive
|
|
# The actual behavior depends on implementation
|
|
assert response.status_code in [200, 401] |