""" 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]