diff --git a/.github/workflows/enterprise-integration-tests.yml b/.github/workflows/enterprise-integration-tests.yml new file mode 100644 index 000000000000..e0e1610adf98 --- /dev/null +++ b/.github/workflows/enterprise-integration-tests.yml @@ -0,0 +1,42 @@ +name: Run Enterprise Integration Tests + +on: + push: + branches: + - main + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ (github.head_ref && github.ref) || github.run_id }} + cancel-in-progress: true + +jobs: + test-enterprise-integration: + name: Enterprise Python Integration Tests + runs-on: ubuntu-24.04 + strategy: + matrix: + python-version: ["3.12"] + steps: + - uses: actions/checkout@v6 + - name: Install poetry via pipx + run: pipx install poetry + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: "poetry" + - name: Install Python dependencies using Poetry + working-directory: ./enterprise + run: poetry install --with dev,test + - name: Run Integration Tests + # Use base working directory for coverage paths to line up. + run: PYTHONPATH=".:$PYTHONPATH" poetry run --project=enterprise pytest --forked -n auto -s -p no:ddtrace -p no:ddtrace.pytest_bdd -p no:ddtrace.pytest_benchmark ./enterprise/tests/integration --cov=enterprise --cov-branch + env: + COVERAGE_FILE: ".coverage.enterprise.integration.${{ matrix.python-version }}" + - name: Store coverage file + uses: actions/upload-artifact@v7 + with: + name: coverage-enterprise-integration + path: ".coverage.enterprise.integration.${{ matrix.python-version }}" + include-hidden-files: true diff --git a/enterprise/pyproject.toml b/enterprise/pyproject.toml index b029d2178f11..c94b65397cde 100644 --- a/enterprise/pyproject.toml +++ b/enterprise/pyproject.toml @@ -92,6 +92,9 @@ lint.pydocstyle.convention = "google" [tool.pytest.ini_options] asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "function" +markers = [ + "integration: mark test as requiring a real database (deselect with '-m not integration')", +] [tool.coverage.run] relative_files = true diff --git a/enterprise/tests/integration/__init__.py b/enterprise/tests/integration/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/enterprise/tests/integration/conftest.py b/enterprise/tests/integration/conftest.py new file mode 100644 index 000000000000..ec6bf107e36b --- /dev/null +++ b/enterprise/tests/integration/conftest.py @@ -0,0 +1,99 @@ +import os + +import pytest +from sqlalchemy import create_engine +from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine +from sqlalchemy.orm import sessionmaker + +# Import all models so their tables are created. +from storage.api_key import ApiKey # noqa: F401 +from storage.base import Base +from storage.billing_session import BillingSession # noqa: F401 +from storage.conversation_work import ConversationWork # noqa: F401 +from storage.device_code import DeviceCode # noqa: F401 +from storage.feedback import Feedback # noqa: F401 +from storage.github_app_installation import GithubAppInstallation # noqa: F401 +from storage.org import Org # noqa: F401 +from storage.org_git_claim import OrgGitClaim # noqa: F401 +from storage.org_invitation import OrgInvitation # noqa: F401 +from storage.org_member import OrgMember # noqa: F401 +from storage.role import Role # noqa: F401 +from storage.slack_conversation import SlackConversation # noqa: F401 +from storage.stored_conversation_metadata import ( + StoredConversationMetadata, # noqa: F401 +) +from storage.stored_conversation_metadata_saas import ( # noqa: F401 + StoredConversationMetadataSaas, +) +from storage.stored_offline_token import StoredOfflineToken # noqa: F401 +from storage.stripe_customer import StripeCustomer # noqa: F401 +from storage.user import User # noqa: F401 +from storage.user_settings import UserSettings # noqa: F401 + + +def pytest_collection_modifyitems(config, items): + for item in items: + item.add_marker(pytest.mark.integration) + + +@pytest.fixture(autouse=True) +def allow_short_context_windows(): + old = os.environ.get('ALLOW_SHORT_CONTEXT_WINDOWS') + os.environ['ALLOW_SHORT_CONTEXT_WINDOWS'] = 'true' + try: + yield + finally: + if old is None: + os.environ.pop('ALLOW_SHORT_CONTEXT_WINDOWS', None) + else: + os.environ['ALLOW_SHORT_CONTEXT_WINDOWS'] = old + + +@pytest.fixture(scope='function') +def db_path(tmp_path): + """Create a unique temp file path for each test.""" + return str(tmp_path / 'test.db') + + +@pytest.fixture +def engine(db_path): + """Create a sync engine with tables using file-based DB.""" + engine = create_engine( + f'sqlite:///{db_path}', connect_args={'check_same_thread': False} + ) + Base.metadata.create_all(engine) + try: + yield engine + finally: + engine.dispose() + + +@pytest.fixture +def session_maker(engine): + return sessionmaker(bind=engine) + + +@pytest.fixture +def async_engine(engine, db_path): + """Create an async engine using the SAME file-based database. + + Depends on engine so tables are already created before async tests run. + """ + ae = create_async_engine( + f'sqlite+aiosqlite:///{db_path}', + connect_args={'check_same_thread': False}, + ) + try: + yield ae + finally: + ae.dispose() + + +@pytest.fixture +async def async_session_maker(async_engine): + """Create an async session maker bound to the async engine.""" + return async_sessionmaker( + bind=async_engine, + class_=AsyncSession, + expire_on_commit=False, + ) diff --git a/enterprise/tests/unit/test_api_key_store.py b/enterprise/tests/integration/test_api_key_store.py similarity index 100% rename from enterprise/tests/unit/test_api_key_store.py rename to enterprise/tests/integration/test_api_key_store.py diff --git a/enterprise/tests/unit/storage/test_api_key_store.py b/enterprise/tests/integration/test_api_key_store_system_keys.py similarity index 100% rename from enterprise/tests/unit/storage/test_api_key_store.py rename to enterprise/tests/integration/test_api_key_store_system_keys.py diff --git a/enterprise/tests/unit/test_org_store.py b/enterprise/tests/integration/test_org_store.py similarity index 100% rename from enterprise/tests/unit/test_org_store.py rename to enterprise/tests/integration/test_org_store.py