# Integration Testing with Real Infrastructure This directory contains comprehensive integration tests that spin up real infrastructure components using Docker containers. ## Overview Unlike unit tests that mock external dependencies, these integration tests use **real services** to test the application as close to production as possible: - **PostgreSQL** - Real database instead of SQLite in-memory - **Redis** - Real message broker for Celery tasks - **Gotenberg** - Real PDF conversion service - **WebDAV Server** - Real upload target - **SFTP Server** - Real SSH/SFTP server - **MinIO** - Real S3-compatible object storage - **FTP Server** - Real FTP server ## Prerequisites ### Required 1. **Docker** - Must be installed and running ```bash docker --version docker ps # Should work without errors ``` 2. **Python Dependencies** ```bash pip install -r requirements-dev.txt ``` This installs: - `testcontainers` - For managing Docker containers in tests - `pytest` and testing tools - `minio`, `boto3`, `redis` - Client libraries for services - `paramiko` - For SFTP testing ### Optional - Docker Compose (for manual infrastructure setup) - Sufficient disk space (~2GB for Docker images) - Sufficient RAM (~4GB recommended) ## Test Organization ### Test Files | File | Description | Scope | |------|-------------|-------| | `test_upload_webdav_comprehensive.py` | Unit tests with mocks (23 tests) | Fast, no Docker | | `test_upload_webdav_integration.py` | WebDAV integration with real server (10 tests) | Medium, requires Docker | | `test_e2e_full_stack.py` | Full end-to-end with all infrastructure (12+ tests) | Slow, requires Docker | | `fixtures_integration.py` | Reusable fixtures for real services | N/A | ### Test Markers Tests are organized using pytest markers: ```python @pytest.mark.unit # Fast unit tests with mocks @pytest.mark.integration # Integration tests with some real services @pytest.mark.e2e # Full end-to-end with complete stack @pytest.mark.requires_docker # Requires Docker to run @pytest.mark.slow # Takes significant time (>30s) ``` ## Running Tests ### Quick Start - Unit Tests Only (No Docker) ```bash # Run only fast unit tests (mocked, no containers) pytest -m unit -v # Run WebDAV unit tests specifically pytest tests/test_upload_webdav_comprehensive.py -v ``` ### Integration Tests - WebDAV Only ```bash # Run WebDAV integration tests (spins up WebDAV container) pytest tests/test_upload_webdav_integration.py -v # Run specific test pytest tests/test_upload_webdav_integration.py::TestWebDAVIntegration::test_upload_file_to_real_webdav_server -v ``` ### Full End-to-End Tests - Complete Infrastructure ```bash # Run all e2e tests (spins up all infrastructure) pytest -m e2e -v # Run specific infrastructure test pytest tests/test_e2e_full_stack.py::TestFullInfrastructure::test_complete_stack_available -v # Run with real Redis and Celery pytest tests/test_e2e_full_stack.py::TestEndToEndWithRedis -v ``` ### Run Everything ```bash # Run all tests (unit + integration + e2e) pytest tests/test_upload_webdav*.py tests/test_e2e*.py -v # Skip slow tests pytest -m "not slow" -v # Run only Docker-based tests pytest -m requires_docker -v ``` ## Infrastructure Fixtures ### Available Fixtures #### `postgres_container` Starts PostgreSQL 15 in Alpine container. ```python def test_with_postgres(postgres_container): db_url = postgres_container["url"] # Use real PostgreSQL ``` #### `redis_container` Starts Redis 7 for Celery broker/backend. ```python def test_with_redis(redis_container): redis_url = redis_container["url"] # Queue actual tasks ``` #### `gotenberg_container` Starts Gotenberg for PDF conversion. ```python def test_with_gotenberg(gotenberg_container): url = gotenberg_container["url"] # Convert documents ``` #### `webdav_container` Starts WebDAV server (bytemark/webdav). ```python def test_with_webdav(webdav_container): # Upload files, verify on server url = webdav_container["url"] username = webdav_container["username"] # "testuser" password = webdav_container["password"] # "testpass" ``` #### `sftp_container` Starts SFTP server (atmoz/sftp). ```python def test_with_sftp(sftp_container): # Upload via SFTP, verify host = sftp_container["host"] port = sftp_container["port"] ``` #### `minio_container` Starts MinIO (S3-compatible). ```python def test_with_s3(minio_container): # Use boto3 with MinIO access_key = minio_container["access_key"] secret_key = minio_container["secret_key"] ``` #### `ftp_container` Starts FTP server (stilliard/pure-ftpd). ```python def test_with_ftp(ftp_container): # Upload via FTP ``` #### `full_infrastructure` Combined fixture with ALL services. ```python def test_production_like(full_infrastructure): infra = full_infrastructure # Access: postgres, redis, gotenberg, webdav, sftp, minio ``` #### `celery_app` and `celery_worker` Real Celery application with worker. ```python def test_celery_tasks(celery_app, celery_worker): # Queue actual tasks that get processed result = my_task.delay(arg1, arg2) result.get(timeout=30) # Wait for worker to process ``` ## Test Scenarios ### 1. Simple WebDAV Upload Test ```python @pytest.mark.integration @pytest.mark.requires_docker def test_upload_to_webdav(webdav_container, sample_text_file): """Upload file to real WebDAV server and verify.""" from app.tasks.upload_to_webdav import upload_to_webdav with patch("app.tasks.upload_to_webdav.settings") as mock_settings: mock_settings.webdav_url = webdav_container["url"] + "/" mock_settings.webdav_username = webdav_container["username"] mock_settings.webdav_password = webdav_container["password"] # Execute upload result = upload_to_webdav.apply(args=[sample_text_file]).get() # Verify on server response = requests.get( f"{webdav_container['url']}/test.txt", auth=(webdav_container["username"], webdav_container["password"]) ) assert response.status_code == 200 ``` ### 2. End-to-End with Redis and Celery ```python @pytest.mark.e2e def test_async_upload(redis_container, webdav_container, celery_worker, sample_text_file): """Queue task in Redis, worker executes, uploads to WebDAV.""" from app.tasks.upload_to_webdav import upload_to_webdav # Queue task (goes to Redis) result = upload_to_webdav.delay(sample_text_file, file_id=1) # Wait for worker to process while not result.ready(): time.sleep(0.5) # Verify result assert result.get()["status"] == "Completed" ``` ### 3. Full Production Pipeline ```python @pytest.mark.e2e @pytest.mark.slow def test_complete_pipeline(full_infrastructure, celery_worker, db_session_real): """ Test complete workflow: 1. Store in PostgreSQL 2. Queue task in Redis 3. Worker processes 4. Upload to WebDAV 5. Verify all steps """ # See test_document_processing_pipeline in test_e2e_full_stack.py ``` ## Performance Notes ### Container Startup Times | Container | Startup Time | Notes | |-----------|--------------|-------| | PostgreSQL | ~2-3s | Fast | | Redis | ~1-2s | Very fast | | WebDAV | ~2s | Fast | | SFTP | ~3s | Moderate | | MinIO | ~3-4s | Moderate | | FTP | ~3s | Moderate | | Gotenberg | ~5-8s | Slower (Chromium startup) | ### Test Execution Times - **Unit tests**: <1s per test - **Single integration test**: 2-5s (with container) - **E2E with full stack**: 10-30s per test - **Full suite**: 2-5 minutes ### Resource Usage - **Memory**: ~100MB per container - **Disk**: ~500MB total for images - **CPU**: Varies, mostly idle ## Debugging ### View Container Logs ```python def test_debug(webdav_container): container = webdav_container["container"] logs = container.get_logs() print(logs) ``` ### Keep Containers Running Set a breakpoint after test to inspect: ```python def test_inspect(webdav_container): result = upload_file() import pdb; pdb.set_trace() # Container still running here # Manually inspect: docker ps, docker logs, etc. ``` ### Check Container Health ```bash # While tests are running docker ps # See running containers docker logs # View logs docker exec -it sh # Shell into container ``` ## Troubleshooting ### "Docker not found" ```bash # Install Docker # On Ubuntu/Debian: sudo apt-get install docker.io sudo usermod -aG docker $USER # Add user to docker group # Logout and login again ``` ### "Permission denied" for Docker ```bash # Add user to docker group sudo usermod -aG docker $USER # Logout/login or: newgrp docker ``` ### "Port already in use" Containers use random ports, but if issues persist: ```bash docker ps # Check what's running docker stop $(docker ps -q) # Stop all containers ``` ### Tests hang or timeout - Increase timeout values in test code - Check Docker has enough resources (memory/CPU) - Check network connectivity ### Containers not cleaning up ```bash # Manual cleanup docker ps -a | grep testcontainers | awk '{print $1}' | xargs docker rm -f docker volume prune -f ``` ## CI/CD Integration ### GitHub Actions - Current Configuration The DocuElevate CI pipeline (`.github/workflows/ci.yml`) runs all non-e2e tests in a single step for simplicity: - All unit, integration, and Docker-based tests run together - Excludes tests marked `e2e` - Uses `pytest-timeout` (300 seconds per test) to prevent individual tests from hanging ```yaml - name: Run Tests run: > pytest tests/ -v --timeout=300 -m "not e2e" ``` **To run E2E tests locally:** ```bash pytest -m e2e -v ``` **To run all tests including E2E locally:** ```bash pytest tests/ -v ``` ### GitHub Actions with E2E Support (Optional) If you want to enable E2E tests in CI, you need Docker-in-Docker setup: ```yaml name: Integration Tests on: [push, pull_request] jobs: test: runs-on: ubuntu-latest services: docker: image: docker:latest options: --privileged steps: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 with: python-version: '3.11' - name: Install dependencies run: | pip install -r requirements-dev.txt - name: Run integration tests run: | pytest -m "integration or e2e" -v --tb=short ``` ## Best Practices ### 1. Use Appropriate Markers ```python # Fast test - use unit @pytest.mark.unit def test_validation(): ... # Needs Docker - mark it @pytest.mark.requires_docker def test_upload(): ... # Slow test - mark it @pytest.mark.slow def test_large_file(): ... ``` ### 2. Reuse Fixtures (Session Scope) ```python # Good - starts once for all tests in class @pytest.fixture(scope="session") def postgres_container(): ... # Bad - starts/stops for each test @pytest.fixture(scope="function") def postgres_container(): ... ``` ### 3. Clean Up Resources ```python def test_with_temp_files(tmp_path): # tmp_path auto-cleans up file = tmp_path / "test.txt" ... ``` ### 4. Use Timeouts ```python # Always set timeouts for container operations response = requests.get(url, timeout=5) result.get(timeout=30) ``` ### 5. Verify Actual Behavior ```python # Don't just check return values result = upload_file() assert result["status"] == "success" # Also verify the file actually exists on the server! assert file_exists_on_server(filename) ``` ## Coverage Running integration tests significantly improves code coverage: | Test Type | upload_to_webdav.py Coverage | |-----------|------------------------------| | Unit only | ~20% | | + Integration | ~80% | | + E2E | ~95%+ | ```bash # Run with coverage pytest tests/test_upload_webdav*.py --cov=app/tasks/upload_to_webdav --cov-report=html # View report open htmlcov/index.html ``` ## Contributing When adding new upload destinations: 1. **Add unit tests** (with mocks) in `test_upload__comprehensive.py` 2. **Add container fixture** in `fixtures_integration.py` 3. **Add integration tests** in `test_upload__integration.py` 4. **Add e2e scenarios** in `test_e2e_full_stack.py` See WebDAV tests as reference implementation. ## Summary | Test Level | Fixtures | Speed | Realism | Use Case | |------------|----------|-------|---------|----------| | Unit | Mocks | Fast (ms) | Low | Development, TDD | | Integration | 1-2 containers | Medium (s) | Medium | Feature testing | | E2E | Full stack | Slow (10s+) | High | Pre-production validation | Choose the appropriate level based on what you're testing!