# ================================================================ # services/auth/tests/conftest.py # Pytest configuration and shared fixtures for auth service tests # ================================================================ """ Shared test configuration and fixtures for authentication service tests """ import pytest import asyncio import os import sys from typing import AsyncGenerator from unittest.mock import AsyncMock, MagicMock, patch from fastapi.testclient import TestClient from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine from sqlalchemy.orm import sessionmaker import redis.asyncio as redis # Add the app directory to the Python path for imports sys.path.append(os.path.join(os.path.dirname(__file__), '..')) # ================================================================ # TEST DATABASE CONFIGURATION # ================================================================ # Use in-memory SQLite for fast testing TEST_DATABASE_URL = "sqlite+aiosqlite:///:memory:" @pytest.fixture(scope="session") def event_loop(): """Create an instance of the default event loop for the test session.""" loop = asyncio.get_event_loop_policy().new_event_loop() yield loop loop.close() @pytest.fixture(scope="function") async def test_engine(): """Create a test database engine for each test function""" engine = create_async_engine( TEST_DATABASE_URL, echo=False, # Set to True for SQL debugging future=True, pool_pre_ping=True ) try: # Import models and base here to avoid import issues from shared.database.base import Base # Create all tables async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) yield engine # Cleanup async with engine.begin() as conn: await conn.run_sync(Base.metadata.drop_all) except ImportError: # If shared.database.base is not available, create a mock yield engine finally: await engine.dispose() @pytest.fixture(scope="function") async def test_db(test_engine) -> AsyncGenerator[AsyncSession, None]: """Create a test database session for each test function""" async_session = sessionmaker( test_engine, class_=AsyncSession, expire_on_commit=False ) async with async_session() as session: yield session @pytest.fixture(scope="function") def client(test_db): """Create a test client with database dependency override""" try: from app.main import app from app.core.database import get_db def override_get_db(): return test_db app.dependency_overrides[get_db] = override_get_db with TestClient(app) as test_client: yield test_client # Clean up overrides app.dependency_overrides.clear() except ImportError as e: pytest.skip(f"Cannot import app modules: {e}") # ================================================================ # MOCK FIXTURES # ================================================================ @pytest.fixture def mock_redis(): """Mock Redis client for testing rate limiting and session management""" redis_mock = AsyncMock() # Default return values for common operations redis_mock.get.return_value = None redis_mock.incr.return_value = 1 redis_mock.expire.return_value = True redis_mock.delete.return_value = True redis_mock.setex.return_value = True redis_mock.exists.return_value = False return redis_mock @pytest.fixture def mock_rabbitmq(): """Mock RabbitMQ for testing event publishing""" rabbitmq_mock = AsyncMock() # Mock publisher methods rabbitmq_mock.publish.return_value = True rabbitmq_mock.connect.return_value = True rabbitmq_mock.disconnect.return_value = True return rabbitmq_mock @pytest.fixture def mock_external_services(): """Mock external service calls (tenant service, etc.)""" with patch('httpx.AsyncClient') as mock_client: mock_response = AsyncMock() mock_response.status_code = 200 mock_response.json.return_value = {"tenants": []} mock_client.return_value.__aenter__.return_value.get.return_value = mock_response mock_client.return_value.__aenter__.return_value.post.return_value = mock_response yield mock_client # ================================================================ # DATA FIXTURES # ================================================================ @pytest.fixture def valid_user_data(): """Valid user registration data""" return { "email": "test@bakery.es", "password": "TestPassword123", "full_name": "Test User" } @pytest.fixture def valid_user_data_list(): """List of valid user data for multiple users""" return [ { "email": f"test{i}@bakery.es", "password": "TestPassword123", "full_name": f"Test User {i}" } for i in range(1, 6) ] @pytest.fixture def weak_password_data(): """User data with various weak passwords""" return [ {"email": "weak1@bakery.es", "password": "123", "full_name": "Weak 1"}, {"email": "weak2@bakery.es", "password": "password", "full_name": "Weak 2"}, {"email": "weak3@bakery.es", "password": "PASSWORD123", "full_name": "Weak 3"}, {"email": "weak4@bakery.es", "password": "testpassword", "full_name": "Weak 4"}, ] @pytest.fixture def invalid_email_data(): """User data with invalid email formats""" return [ {"email": "invalid", "password": "TestPassword123", "full_name": "Invalid 1"}, {"email": "@bakery.es", "password": "TestPassword123", "full_name": "Invalid 2"}, {"email": "test@", "password": "TestPassword123", "full_name": "Invalid 3"}, {"email": "test..test@bakery.es", "password": "TestPassword123", "full_name": "Invalid 4"}, ] # ================================================================ # USER FIXTURES # ================================================================ @pytest.fixture async def test_user(test_db, valid_user_data): """Create a test user in the database""" try: from app.services.auth_service import AuthService user = await AuthService.create_user( email=valid_user_data["email"], password=valid_user_data["password"], full_name=valid_user_data["full_name"], db=test_db ) return user except ImportError: pytest.skip("AuthService not available") @pytest.fixture async def test_users(test_db, valid_user_data_list): """Create multiple test users in the database""" try: from app.services.auth_service import AuthService users = [] for user_data in valid_user_data_list: user = await AuthService.create_user( email=user_data["email"], password=user_data["password"], full_name=user_data["full_name"], db=test_db ) users.append(user) return users except ImportError: pytest.skip("AuthService not available") @pytest.fixture async def authenticated_user(client, valid_user_data): """Create an authenticated user and return user info, tokens, and headers""" # Register user register_response = client.post("/auth/register", json=valid_user_data) assert register_response.status_code == 200 # Login user login_data = { "email": valid_user_data["email"], "password": valid_user_data["password"] } login_response = client.post("/auth/login", json=login_data) assert login_response.status_code == 200 token_data = login_response.json() return { "user": register_response.json(), "tokens": token_data, "access_token": token_data["access_token"], "refresh_token": token_data["refresh_token"], "headers": {"Authorization": f"Bearer {token_data['access_token']}"} } # ================================================================ # CONFIGURATION FIXTURES # ================================================================ @pytest.fixture def test_settings(): """Test-specific settings override""" try: from app.core.config import settings original_settings = {} # Store original values test_overrides = { 'JWT_ACCESS_TOKEN_EXPIRE_MINUTES': 30, 'JWT_REFRESH_TOKEN_EXPIRE_DAYS': 7, 'PASSWORD_MIN_LENGTH': 8, 'PASSWORD_REQUIRE_UPPERCASE': True, 'PASSWORD_REQUIRE_LOWERCASE': True, 'PASSWORD_REQUIRE_NUMBERS': True, 'PASSWORD_REQUIRE_SYMBOLS': False, 'MAX_LOGIN_ATTEMPTS': 5, 'LOCKOUT_DURATION_MINUTES': 30, 'BCRYPT_ROUNDS': 4, # Lower for faster tests } for key, value in test_overrides.items(): if hasattr(settings, key): original_settings[key] = getattr(settings, key) setattr(settings, key, value) yield settings # Restore original values for key, value in original_settings.items(): setattr(settings, key, value) except ImportError: pytest.skip("Settings not available") # ================================================================ # PATCHING FIXTURES # ================================================================ @pytest.fixture def patch_redis(mock_redis): """Patch Redis client for all tests""" with patch('app.core.security.redis_client', mock_redis): yield mock_redis @pytest.fixture def patch_messaging(mock_rabbitmq): """Patch messaging system for all tests""" with patch('app.services.messaging.publisher', mock_rabbitmq): yield mock_rabbitmq @pytest.fixture def patch_external_apis(mock_external_services): """Patch external API calls""" yield mock_external_services # ================================================================ # UTILITY FIXTURES # ================================================================ @pytest.fixture def auth_headers(): """Factory for creating authorization headers""" def _create_headers(token): return {"Authorization": f"Bearer {token}"} return _create_headers @pytest.fixture def password_generator(): """Generate passwords with different characteristics""" def _generate( length=12, include_upper=True, include_lower=True, include_numbers=True, include_symbols=False ): import random import string chars = "" password = "" if include_lower: chars += string.ascii_lowercase password += random.choice(string.ascii_lowercase) if include_upper: chars += string.ascii_uppercase password += random.choice(string.ascii_uppercase) if include_numbers: chars += string.digits password += random.choice(string.digits) if include_symbols: chars += "!@#$%^&*" password += random.choice("!@#$%^&*") # Fill remaining length remaining = length - len(password) if remaining > 0: password += ''.join(random.choice(chars) for _ in range(remaining)) # Shuffle the password password_list = list(password) random.shuffle(password_list) return ''.join(password_list) return _generate # ================================================================ # PERFORMANCE TESTING FIXTURES # ================================================================ @pytest.fixture def performance_timer(): """Timer utility for performance testing""" import time class Timer: def __init__(self): self.start_time = None self.end_time = None def start(self): self.start_time = time.time() def stop(self): self.end_time = time.time() @property def elapsed(self): if self.start_time and self.end_time: return self.end_time - self.start_time return None def __enter__(self): self.start() return self def __exit__(self, *args): self.stop() return Timer # ================================================================ # DATABASE UTILITY FIXTURES # ================================================================ @pytest.fixture async def db_utils(test_db): """Database utility functions for testing""" class DBUtils: def __init__(self, db): self.db = db async def count_users(self): try: from sqlalchemy import select, func from app.models.users import User result = await self.db.execute(select(func.count(User.id))) return result.scalar() except ImportError: return 0 async def get_user_by_email(self, email): try: from sqlalchemy import select from app.models.users import User result = await self.db.execute(select(User).where(User.email == email)) return result.scalar_one_or_none() except ImportError: return None async def count_refresh_tokens(self): try: from sqlalchemy import select, func from app.models.users import RefreshToken result = await self.db.execute(select(func.count(RefreshToken.id))) return result.scalar() except ImportError: return 0 async def clear_all_data(self): try: from app.models.users import User, RefreshToken await self.db.execute(RefreshToken.__table__.delete()) await self.db.execute(User.__table__.delete()) await self.db.commit() except ImportError: pass return DBUtils(test_db) # ================================================================ # LOGGING FIXTURES # ================================================================ @pytest.fixture def capture_logs(): """Capture logs for testing""" import logging from io import StringIO log_capture = StringIO() handler = logging.StreamHandler(log_capture) handler.setLevel(logging.DEBUG) # Add handler to auth service loggers loggers = [ logging.getLogger('app.services.auth_service'), logging.getLogger('app.core.security'), logging.getLogger('app.api.auth'), ] for logger in loggers: logger.addHandler(handler) logger.setLevel(logging.DEBUG) yield log_capture # Clean up for logger in loggers: logger.removeHandler(handler) # ================================================================ # TEST MARKERS AND CONFIGURATION # ================================================================ def pytest_configure(config): """Configure pytest with custom markers""" config.addinivalue_line( "markers", "unit: marks tests as unit tests" ) config.addinivalue_line( "markers", "integration: marks tests as integration tests" ) config.addinivalue_line( "markers", "api: marks tests as API tests" ) config.addinivalue_line( "markers", "security: marks tests as security tests" ) config.addinivalue_line( "markers", "performance: marks tests as performance tests" ) config.addinivalue_line( "markers", "slow: marks tests as slow running" ) config.addinivalue_line( "markers", "auth: marks tests as authentication tests" ) def pytest_collection_modifyitems(config, items): """Modify test collection to add markers automatically""" for item in items: # Add markers based on test class or function names if "test_api" in item.name.lower() or "API" in str(item.cls): item.add_marker(pytest.mark.api) if "test_security" in item.name.lower() or "Security" in str(item.cls): item.add_marker(pytest.mark.security) if "test_performance" in item.name.lower() or "Performance" in str(item.cls): item.add_marker(pytest.mark.performance) item.add_marker(pytest.mark.slow) if "integration" in item.name.lower() or "Integration" in str(item.cls): item.add_marker(pytest.mark.integration) if "Flow" in str(item.cls) or "flow" in item.name.lower(): item.add_marker(pytest.mark.integration) if "auth" in item.name.lower() or "Auth" in str(item.cls): item.add_marker(pytest.mark.auth)