Add pytest tests to auth 6
This commit is contained in:
@@ -1,225 +1,173 @@
|
||||
# ================================================================
|
||||
# services/auth/tests/conftest.py
|
||||
# ================================================================
|
||||
"""
|
||||
Simple pytest configuration for auth service with mock database
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
import pytest_asyncio
|
||||
import uuid
|
||||
from typing import AsyncGenerator
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
|
||||
from sqlalchemy.pool import StaticPool
|
||||
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 - using in-memory SQLite for simplicity
|
||||
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()
|
||||
# Create test engine
|
||||
test_engine = create_async_engine(
|
||||
TEST_DATABASE_URL,
|
||||
connect_args={"check_same_thread": False},
|
||||
poolclass=StaticPool,
|
||||
echo=False
|
||||
)
|
||||
|
||||
@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
|
||||
)
|
||||
# Import Base and metadata after engine creation to avoid circular imports
|
||||
from shared.database.base import Base
|
||||
async with engine.begin() as conn:
|
||||
# Create async session maker
|
||||
TestingSessionLocal = async_sessionmaker(
|
||||
test_engine,
|
||||
class_=AsyncSession,
|
||||
expire_on_commit=False
|
||||
)
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def mock_db() -> AsyncGenerator[AsyncMock, None]:
|
||||
"""Create a mock database session for testing"""
|
||||
mock_session = AsyncMock(spec=AsyncSession)
|
||||
|
||||
# Configure common mock behaviors
|
||||
mock_session.commit = AsyncMock()
|
||||
mock_session.rollback = AsyncMock()
|
||||
mock_session.close = AsyncMock()
|
||||
mock_session.refresh = AsyncMock()
|
||||
mock_session.add = Mock()
|
||||
mock_session.execute = AsyncMock()
|
||||
mock_session.scalar = AsyncMock()
|
||||
mock_session.scalars = AsyncMock()
|
||||
|
||||
yield mock_session
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def real_test_db() -> AsyncGenerator[AsyncSession, None]:
|
||||
"""Create a real test database session (in-memory SQLite)"""
|
||||
# Import here to avoid circular imports
|
||||
from app.core.database import Base
|
||||
|
||||
async with test_engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
yield engine
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.drop_all)
|
||||
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:
|
||||
|
||||
async with TestingSessionLocal() as session:
|
||||
yield session
|
||||
await session.rollback() # Rollback after each test to ensure a clean state
|
||||
|
||||
async with test_engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.drop_all)
|
||||
|
||||
@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
|
||||
@pytest.fixture
|
||||
def mock_redis():
|
||||
"""Create a mock Redis client"""
|
||||
mock_redis = AsyncMock()
|
||||
mock_redis.get = AsyncMock(return_value=None)
|
||||
mock_redis.set = AsyncMock(return_value=True)
|
||||
mock_redis.setex = AsyncMock(return_value=True) # Add setex method
|
||||
mock_redis.delete = AsyncMock(return_value=1)
|
||||
mock_redis.incr = AsyncMock(return_value=1)
|
||||
mock_redis.expire = AsyncMock(return_value=True)
|
||||
return mock_redis
|
||||
|
||||
def override_get_db():
|
||||
# test_db is already an AsyncSession yielded by the fixture
|
||||
yield test_db
|
||||
@pytest.fixture
|
||||
def test_client():
|
||||
"""Create a test client for the FastAPI app"""
|
||||
from app.main import app
|
||||
return TestClient(app)
|
||||
|
||||
app.dependency_overrides[get_db] = override_get_db
|
||||
@pytest.fixture
|
||||
def test_tenant_id():
|
||||
"""Generate a test tenant ID"""
|
||||
return uuid.uuid4()
|
||||
|
||||
with TestClient(app) as test_client:
|
||||
yield test_client
|
||||
@pytest.fixture
|
||||
def test_user_data():
|
||||
"""Generate test user data"""
|
||||
unique_id = uuid.uuid4().hex[:8]
|
||||
return {
|
||||
"email": f"test_{unique_id}@bakery.es",
|
||||
"password": "TestPassword123!",
|
||||
"full_name": f"Test User {unique_id}",
|
||||
"tenant_id": uuid.uuid4()
|
||||
}
|
||||
|
||||
# Clean up overrides
|
||||
app.dependency_overrides.clear()
|
||||
except ImportError as e:
|
||||
pytest.skip(f"Cannot import app modules: {e}. Ensure app.main and app.core.database are accessible.")
|
||||
@pytest.fixture
|
||||
def test_user_create_data():
|
||||
"""Generate user creation data for database"""
|
||||
return {
|
||||
"id": uuid.uuid4(),
|
||||
"email": "test@bakery.es",
|
||||
"full_name": "Test User",
|
||||
"hashed_password": "$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewDtmRhckC.wSqDa", # "password123"
|
||||
"is_active": True,
|
||||
"tenant_id": uuid.uuid4(),
|
||||
"created_at": "2024-01-01T00:00:00",
|
||||
"updated_at": "2024-01-01T00:00:00"
|
||||
}
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
async def test_user(test_db):
|
||||
"""Create a test user in the database"""
|
||||
try:
|
||||
from app.services.auth_service import AuthService
|
||||
from app.schemas.auth import UserRegistration
|
||||
@pytest.fixture
|
||||
def mock_user():
|
||||
"""Create a mock user object"""
|
||||
mock_user = Mock()
|
||||
mock_user.id = uuid.uuid4()
|
||||
mock_user.email = "test@bakery.es"
|
||||
mock_user.full_name = "Test User"
|
||||
mock_user.is_active = True
|
||||
mock_user.is_verified = False
|
||||
mock_user.tenant_id = uuid.uuid4()
|
||||
mock_user.hashed_password = "$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewDtmRhckC.wSqDa"
|
||||
mock_user.created_at = "2024-01-01T00:00:00"
|
||||
mock_user.updated_at = "2024-01-01T00:00:00"
|
||||
return mock_user
|
||||
|
||||
user_data = UserRegistration(
|
||||
email="existing@bakery.es",
|
||||
password="TestPassword123",
|
||||
full_name="Existing User"
|
||||
)
|
||||
@pytest.fixture
|
||||
def mock_tokens():
|
||||
"""Create mock JWT tokens"""
|
||||
return {
|
||||
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ",
|
||||
"refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ",
|
||||
"token_type": "bearer"
|
||||
}
|
||||
|
||||
user = await AuthService.create_user(
|
||||
email=user_data.email,
|
||||
password=user_data.password,
|
||||
full_name=user_data.full_name,
|
||||
db=test_db
|
||||
)
|
||||
return user
|
||||
except ImportError:
|
||||
pytest.skip("AuthService not available")
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
async def test_redis_client():
|
||||
"""Create a test Redis client"""
|
||||
# Use a mock Redis client for testing
|
||||
mock_redis = AsyncMock(spec=redis.Redis)
|
||||
yield mock_redis
|
||||
await mock_redis.close()
|
||||
|
||||
# ================================================================\
|
||||
# TEST HELPERS
|
||||
# ================================================================\
|
||||
|
||||
import uuid # Moved from test_auth_comprehensive.py as it's a shared helper
|
||||
@pytest.fixture
|
||||
def auth_headers(mock_tokens):
|
||||
"""Create authorization headers for testing"""
|
||||
return {"Authorization": f"Bearer {mock_tokens['access_token']}"}
|
||||
|
||||
def generate_random_user_data(prefix="test"):
|
||||
"""Generates unique user data for testing."""
|
||||
"""Generate unique user data for testing"""
|
||||
unique_id = uuid.uuid4().hex[:8]
|
||||
return {
|
||||
"email": f"{prefix}_{unique_id}@bakery.es",
|
||||
"password": f"StrongPwd{unique_id}!",
|
||||
"password": f"TestPassword{unique_id}!",
|
||||
"full_name": f"Test User {unique_id}"
|
||||
}
|
||||
|
||||
# ================================================================\
|
||||
# PYTEST HOOKS
|
||||
# ================================================================\
|
||||
|
||||
def pytest_addoption(parser):
|
||||
"""Add custom options to pytest"""
|
||||
parser.addoption(
|
||||
"--integration", action="store_true", default=False, help="run integration tests"
|
||||
)
|
||||
parser.addoption(
|
||||
"--api", action="store_true", default=False, help="run API tests"
|
||||
)
|
||||
parser.addoption(
|
||||
"--security", action="store_true", default=False, help="run security tests"
|
||||
)
|
||||
parser.addoption(
|
||||
"--performance", action="store_true", default=False, help="run performance tests"
|
||||
)
|
||||
parser.addoption(
|
||||
"--slow", action="store_true", default=False, help="run slow tests"
|
||||
)
|
||||
parser.addoption(
|
||||
"--auth", action="store_true", default=False, help="run authentication tests"
|
||||
)
|
||||
|
||||
# Pytest configuration
|
||||
def pytest_configure(config):
|
||||
"""Configure pytest 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"
|
||||
)
|
||||
config.addinivalue_line("markers", "unit: Unit tests")
|
||||
config.addinivalue_line("markers", "integration: Integration tests")
|
||||
config.addinivalue_line("markers", "api: API endpoint tests")
|
||||
config.addinivalue_line("markers", "security: Security-related tests")
|
||||
config.addinivalue_line("markers", "slow: Slow-running 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) # Authentication flows are integration tests
|
||||
|
||||
# Mark all tests in test_auth_comprehensive.py with 'auth'
|
||||
if "test_auth_comprehensive" in str(item.fspath):
|
||||
item.add_marker(pytest.mark.auth)
|
||||
|
||||
# Filtering logic for command line options
|
||||
if not any([config.getoption("--integration"), config.getoption("--api"),
|
||||
config.getoption("--security"), config.getoption("--performance"),
|
||||
config.getoption("--slow"), config.getoption("--auth")]):
|
||||
return # No specific filter applied, run all collected tests
|
||||
|
||||
skip_markers = []
|
||||
if not config.getoption("--integration"):
|
||||
skip_markers.append(pytest.mark.integration)
|
||||
if not config.getoption("--api"):
|
||||
skip_markers.append(pytest.mark.api)
|
||||
if not config.getoption("--security"):
|
||||
skip_markers.append(pytest.mark.security)
|
||||
if not config.getoption("--performance"):
|
||||
skip_markers.append(pytest.mark.performance)
|
||||
if not config.getoption("--slow"):
|
||||
skip_markers.append(pytest.mark.slow)
|
||||
if not config.getoption("--auth"):
|
||||
skip_markers.append(pytest.mark.auth)
|
||||
|
||||
# Remove tests with any of the skip markers
|
||||
if skip_markers:
|
||||
for item in list(items): # Iterate over a copy to allow modification
|
||||
if any(marker in item.iter_markers() for marker in skip_markers):
|
||||
items.remove(item)
|
||||
item.add_marker(pytest.mark.skip(reason="filtered by command line option"))
|
||||
# Mock environment variables for testing
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_env_vars(monkeypatch):
|
||||
"""Mock environment variables for testing"""
|
||||
monkeypatch.setenv("JWT_SECRET_KEY", "test-secret-key-for-testing")
|
||||
monkeypatch.setenv("JWT_ACCESS_TOKEN_EXPIRE_MINUTES", "30")
|
||||
monkeypatch.setenv("JWT_REFRESH_TOKEN_EXPIRE_DAYS", "7")
|
||||
monkeypatch.setenv("MAX_LOGIN_ATTEMPTS", "5")
|
||||
monkeypatch.setenv("LOCKOUT_DURATION_MINUTES", "30")
|
||||
monkeypatch.setenv("DATABASE_URL", TEST_DATABASE_URL)
|
||||
monkeypatch.setenv("REDIS_URL", "redis://localhost:6379/1")
|
||||
monkeypatch.setenv("RABBITMQ_URL", "amqp://guest:guest@localhost:5672/")
|
||||
Reference in New Issue
Block a user