diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..2180bc1 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,15 @@ +"""Pytest configuration and fixtures.""" + +import os + +import pytest + + +@pytest.fixture(scope="session", autouse=True) +def setup_test_env(): + """Set up test environment variables for all tests.""" + os.environ["OPENAI_API_KEY"] = "test-key-for-testing" + # Use PostgreSQL from docker-compose for testing + # Default credentials match docker-compose.yml defaults + # Note: Port 54320 is the host-mapped port from docker compose + os.environ["DATABASE_URL"] = "postgresql+psycopg://postgres:postgres@localhost:54320/memu" diff --git a/tests/test_env_validation.py b/tests/test_env_validation.py new file mode 100644 index 0000000..f49bf2f --- /dev/null +++ b/tests/test_env_validation.py @@ -0,0 +1,118 @@ +"""Test environment variable validation using subprocess isolation. + +This module uses subprocess isolation to test module-level initialization errors. +This approach avoids issues with shared module state and ensures clean test isolation. +""" + +import subprocess +import sys +from pathlib import Path + +# Project root directory (parent of tests/) +PROJECT_ROOT = Path(__file__).parent.parent + + +def _run_import_test(env_vars: dict[str, str], remove_vars: list[str] | None = None) -> subprocess.CompletedProcess: + """ + Run a subprocess that attempts to import app.main with specified environment. + + Args: + env_vars: Environment variables to set + remove_vars: Environment variables to remove (if any) + + Returns: + CompletedProcess with returncode, stdout, and stderr + """ + import os + + # Start with a clean environment based on current env + env = os.environ.copy() + + # Remove specified variables + if remove_vars: + for var in remove_vars: + env.pop(var, None) + + # Set specified variables + env.update(env_vars) + + # Run Python subprocess that imports app.main + result = subprocess.run( + [sys.executable, "-c", "from app.main import app; print(app.title)"], + env=env, + capture_output=True, + text=True, + cwd=str(PROJECT_ROOT), + timeout=30, # Prevent tests from hanging indefinitely + ) + return result + + +def test_app_requires_openai_api_key(): + """Test that app refuses to start when OPENAI_API_KEY is not set.""" + result = _run_import_test( + env_vars={ + "DATABASE_URL": "postgresql+psycopg://test:test@localhost:5432/test", + }, + remove_vars=["OPENAI_API_KEY"], + ) + + assert result.returncode != 0 + assert "OPENAI_API_KEY environment variable is not set or is empty" in result.stderr + + +def test_app_refuses_empty_openai_api_key(): + """Test that app refuses to start when OPENAI_API_KEY is empty.""" + result = _run_import_test( + env_vars={ + "OPENAI_API_KEY": "", + "DATABASE_URL": "postgresql+psycopg://test:test@localhost:5432/test", + }, + ) + + assert result.returncode != 0 + assert "OPENAI_API_KEY environment variable is not set or is empty" in result.stderr + + +def test_app_requires_database_url(): + """Test that app refuses to start when DATABASE_URL is not set.""" + result = _run_import_test( + env_vars={ + "OPENAI_API_KEY": "test-key", + }, + remove_vars=["DATABASE_URL", "DATABASE_HOST", "DATABASE_USER", "DATABASE_PASSWORD", "DATABASE_NAME"], + ) + + assert result.returncode != 0 + assert "Database configuration is incomplete" in result.stderr + + +def test_app_with_individual_db_vars(): + """Test that app starts with individual DATABASE_* variables.""" + result = _run_import_test( + env_vars={ + "OPENAI_API_KEY": "test-key", + "DATABASE_HOST": "localhost", + "DATABASE_PORT": "54320", + "DATABASE_USER": "test_user", + "DATABASE_PASSWORD": "test_pass", + "DATABASE_NAME": "test_db", + }, + remove_vars=["DATABASE_URL"], + ) + + assert result.returncode == 0 + assert "memU Server" in result.stdout + + +def test_app_starts_with_valid_openai_api_key(): + """Test that app starts successfully with valid OPENAI_API_KEY.""" + result = _run_import_test( + env_vars={ + "OPENAI_API_KEY": "test-valid-key", + "DATABASE_URL": "postgresql+psycopg://test:test@localhost:5432/test", + }, + ) + + assert result.returncode == 0 + assert "memU Server" in result.stdout diff --git a/tests/test_health.py b/tests/test_health.py new file mode 100644 index 0000000..625ce4b --- /dev/null +++ b/tests/test_health.py @@ -0,0 +1,24 @@ +"""Tests for the application's root ("/") endpoint.""" + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture(scope="module") +def client(): + """Create FastAPI test client with proper env setup.""" + try: + from app.main import app + + return TestClient(app) + except Exception as exc: + pytest.skip(f"Could not initialize test client due to application setup error: {exc}") + + +def test_root_endpoint(client): + """Test root endpoint returns welcome message.""" + response = client.get("/") + assert response.status_code == 200 + data = response.json() + assert "message" in data + assert data["message"] == "Hello MemU user!"