# conftest.py - PostgreSQL test configuration (project root) """ Core pytest configuration and fixtures. This project uses PostgreSQL for testing. Start the test database with: make test-db-up IMPORTANT - Fixture Best Practices: =================================== 1. DO NOT use db.expunge() on fixtures - it detaches objects from the session and breaks lazy loading of relationships (e.g., product.marketplace_product). 2. Test isolation is achieved through TRUNCATE CASCADE after each test, which is much faster than dropping/recreating tables. 3. If you need to ensure an object has fresh data, use db.refresh(obj) instead of expunge/re-query patterns. 4. The session uses expire_on_commit=False to prevent objects from being expired after commits, which helps with relationship access across operations. See docs/testing/testing-guide.md for comprehensive testing documentation. """ import os import pytest from fastapi.testclient import TestClient from sqlalchemy import create_engine, text from sqlalchemy.orm import sessionmaker from app.core.database import Base, get_db from main import app # PostgreSQL test database URL # Use environment variable or default to local Docker test database TEST_DATABASE_URL = os.getenv( "TEST_DATABASE_URL", "postgresql://test_user:test_password@localhost:5433/orion_test" ) @pytest.fixture(scope="session") def engine(): """Create test database engine. Verifies PostgreSQL connection on startup and provides helpful error message if the test database is not running. """ engine = create_engine( TEST_DATABASE_URL, pool_pre_ping=True, echo=False, # Set to True for SQL debugging ) # Verify connection on startup try: with engine.connect() as conn: conn.execute(text("SELECT 1")) except Exception as e: pytest.exit( f"\n\nCannot connect to test database at {TEST_DATABASE_URL}\n" f"Error: {e}\n\n" "Start the test database with:\n" " make test-db-up\n\n" "Or manually:\n" " docker-compose -f docker-compose.test.yml up -d\n" ) return engine @pytest.fixture(scope="session") def testing_session_local(engine): """ Create session factory for tests. Uses expire_on_commit=False to prevent objects from being expired after commits. This allows fixtures to remain usable after database operations without needing to refresh or re-query them. """ from app.core.database import register_soft_delete_filter session_factory = sessionmaker( autocommit=False, autoflush=False, bind=engine, expire_on_commit=False, # Prevents lazy-load issues after commits ) register_soft_delete_filter(session_factory) return session_factory @pytest.fixture(scope="session", autouse=True) def setup_database(engine): """Create all tables once at the start of the test session.""" with engine.connect() as conn: # Terminate any other connections to avoid deadlocks conn.execute(text(""" SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = current_database() AND pid <> pg_backend_pid() """)) conn.execute(text("DROP SCHEMA public CASCADE")) conn.execute(text("CREATE SCHEMA public")) conn.commit() Base.metadata.create_all(bind=engine) yield @pytest.fixture(scope="function") def db(engine, testing_session_local): """ Create a database session for each test function. Provides test isolation by: - Using a fresh session for each test - Truncating all tables after each test (fast cleanup) Note: Fixtures should NOT use db.expunge() as this detaches objects from the session and breaks lazy loading. The TRUNCATE provides sufficient isolation between tests. """ db_session = testing_session_local() try: yield db_session finally: db_session.close() # Fast cleanup with TRUNCATE CASCADE with engine.connect() as conn: # Disable FK checks temporarily for fast truncation conn.execute(text("SET session_replication_role = 'replica'")) for table in reversed(Base.metadata.sorted_tables): conn.execute(text(f'TRUNCATE TABLE "{table.name}" CASCADE')) # noqa: SEC011 conn.execute(text("SET session_replication_role = 'origin'")) conn.commit() @pytest.fixture(scope="function") def client(db): """Create a test client with database dependency override""" # Override the dependency to use our test database def override_get_db(): try: yield db finally: pass # Don't close here, the db fixture handles it app.dependency_overrides[get_db] = override_get_db try: client = TestClient(app) yield client finally: # Clean up the dependency override if get_db in app.dependency_overrides: del app.dependency_overrides[get_db] # Cleanup fixture to ensure clean state @pytest.fixture(autouse=True) def cleanup(): """Automatically clean up after each test""" yield # Clear any remaining dependency overrides app.dependency_overrides.clear() # Import fixtures from fixture modules pytest_plugins = [ "tests.fixtures.admin_platform_fixtures", "tests.fixtures.auth_fixtures", "tests.fixtures.loyalty_fixtures", "tests.fixtures.marketplace_product_fixtures", "tests.fixtures.store_fixtures", "tests.fixtures.customer_fixtures", "tests.fixtures.marketplace_import_job_fixtures", "tests.fixtures.message_fixtures", "tests.fixtures.testing_fixtures", "tests.fixtures.content_page_fixtures", "tests.fixtures.merchant_domain_fixtures", ]