# Backend E2E Testing Guide

End-to-end testing infrastructure using **Testcontainers** (real PostgreSQL) and **Schemathesis** (OpenAPI contract testing).

## Table of Contents

- [Quick Start](#quick-start)
- [Requirements](#requirements)
- [How It Works](#how-it-works)
- [Test Organization](#test-organization)
- [Writing E2E Tests](#writing-e2e-tests)
- [Running Tests](#running-tests)
- [Troubleshooting](#troubleshooting)
- [CI/CD Integration](#cicd-integration)

---

## Quick Start

```bash
# 1. Install E2E dependencies
make install-e2e

# 2. Ensure Docker is running
make check-docker

# 3. Run E2E tests
make test-e2e
```

---

## Requirements

### Docker

E2E tests use Testcontainers to spin up real PostgreSQL containers. Docker must be running:

- **macOS/Windows**: Docker Desktop
- **Linux**: Docker Engine (`sudo systemctl start docker`)

### Dependencies

E2E tests require additional packages beyond the standard dev dependencies:

```bash
# Install E2E dependencies
make install-e2e

# Or manually:
uv sync --extra dev --extra e2e
```

This installs:
- `testcontainers[postgres]>=4.0.0` - Docker container management
- `schemathesis>=3.30.0` - OpenAPI contract testing

---

## How It Works

### Testcontainers

Testcontainers automatically manages Docker containers for tests:

1. **Session-scoped container**: A single PostgreSQL 17 container starts once per test session
2. **Function-scoped isolation**: Each test gets fresh tables (drop + recreate)
3. **Automatic cleanup**: Container is destroyed when tests complete

This approach catches bugs that SQLite-based tests miss:
- PostgreSQL-specific SQL behavior
- Real constraint violations
- Actual transaction semantics
- JSONB column behavior

### Schemathesis

Schemathesis generates test cases from your OpenAPI schema:

1. **Schema loading**: Reads `/api/v1/openapi.json` from your FastAPI app
2. **Test generation**: Creates test cases for each endpoint
3. **Response validation**: Verifies responses match documented schema

This catches:
- Undocumented response codes
- Schema mismatches (wrong types, missing fields)
- Edge cases in input validation

---

## Test Organization

```
backend/tests/
├── e2e/                          # E2E tests (PostgreSQL, Docker required)
│   ├── __init__.py
│   ├── conftest.py               # Testcontainers fixtures
│   ├── test_api_contracts.py     # Schemathesis schema tests
│   └── test_database_workflows.py # PostgreSQL workflow tests
│
├── api/                          # Integration tests (SQLite, fast)
├── repositories/                 # Repository unit tests
└── conftest.py                   # Standard fixtures
```

### Test Markers

Tests use pytest markers for filtering:

| Marker | Description |
|--------|-------------|
| `@pytest.mark.e2e` | End-to-end test requiring Docker |
| `@pytest.mark.postgres` | PostgreSQL-specific test |
| `@pytest.mark.schemathesis` | Schemathesis schema test |

---

## Writing E2E Tests

### Basic E2E Test

```python
import pytest
from uuid import uuid4

@pytest.mark.e2e
@pytest.mark.postgres
@pytest.mark.asyncio
async def test_user_workflow(e2e_client):
    """Test user registration with real PostgreSQL."""
    email = f"test-{uuid4().hex[:8]}@example.com"

    response = await e2e_client.post(
        "/api/v1/auth/register",
        json={
            "email": email,
            "password": "SecurePassword123!",
            "first_name": "Test",
            "last_name": "User",
        },
    )

    assert response.status_code in [200, 201]
    assert response.json()["email"] == email
```

### Available Fixtures

| Fixture | Scope | Description |
|---------|-------|-------------|
| `postgres_container` | session | Raw Testcontainers PostgreSQL container |
| `async_postgres_url` | session | Asyncpg-compatible connection URL |
| `e2e_db_session` | function | SQLAlchemy AsyncSession with fresh tables |
| `e2e_client` | function | httpx AsyncClient connected to real DB |

### Schemathesis Test

```python
import pytest
import schemathesis
from hypothesis import settings, Phase

from app.main import app

schema = schemathesis.from_asgi("/api/v1/openapi.json", app=app)

@pytest.mark.e2e
@pytest.mark.schemathesis
@schema.parametrize(endpoint="/api/v1/auth/register")
@settings(max_examples=20)
def test_registration_schema(case):
    """Test registration endpoint conforms to schema."""
    response = case.call_asgi()
    case.validate_response(response)
```

---

## Running Tests

### Commands

```bash
# Run all E2E tests
make test-e2e

# Run only Schemathesis schema tests
make test-e2e-schema

# Run all tests (unit + integration + E2E)
make test-all

# Check Docker availability
make check-docker
```

### Direct pytest

```bash
# All E2E tests
IS_TEST=True PYTHONPATH=. uv run pytest tests/e2e/ -v

# Only PostgreSQL tests
IS_TEST=True PYTHONPATH=. uv run pytest tests/e2e/ -v -m postgres

# Only Schemathesis tests
IS_TEST=True PYTHONPATH=. uv run pytest tests/e2e/ -v -m schemathesis
```

---

## Troubleshooting

### Docker Not Running

**Error:**
```
Docker is not running!
E2E tests require Docker to be running.
```

**Solution:**
```bash
# macOS/Windows
# Open Docker Desktop

# Linux
sudo systemctl start docker
```

### Testcontainers Not Installed

**Error:**
```
SKIPPED: testcontainers not installed - run: make install-e2e
```

**Solution:**
```bash
make install-e2e
# Or: uv sync --extra dev --extra e2e
```

### Container Startup Timeout

**Error:**
```
testcontainers.core.waiting_utils.UnexpectedResponse
```

**Solutions:**
1. Increase Docker resources (memory, CPU)
2. Pull the image manually: `docker pull postgres:17-alpine`
3. Check Docker daemon logs: `docker logs`

### Port Conflicts

**Error:**
```
Error starting container: port is already allocated
```

**Solution:**
Testcontainers uses random ports, so conflicts are rare. If occurring:
1. Stop other PostgreSQL containers: `docker stop $(docker ps -q)`
2. Check for orphaned containers: `docker container prune`

### Ryuk/Reaper Port 8080 Issues

**Error:**
```
ConnectionError: Port mapping for container ... and port 8080 is not available
```

**Solution:**
This is related to the Testcontainers Reaper (Ryuk) which handles automatic cleanup.
The `conftest.py` automatically disables Ryuk to avoid this issue. If you still encounter
this error, ensure you're using the latest conftest.py or set the environment variable:

```bash
export TESTCONTAINERS_RYUK_DISABLED=true
```

### Parallel Test Execution Issues

**Error:**
```
ScopeMismatch: ... cannot use a higher-scoped fixture 'postgres_container'
```

**Solution:**
E2E tests must run sequentially (not in parallel) because they share a session-scoped
PostgreSQL container. The Makefile commands use `-n 0` to disable parallel execution.
If running pytest directly, add `-n 0`:

```bash
IS_TEST=True PYTHONPATH=. uv run pytest tests/e2e/ -v -n 0
```

---

## CI/CD Integration

### GitHub Actions

A workflow template is provided at `.github/workflows/backend-e2e-tests.yml.template`.

To enable:
1. Rename to `backend-e2e-tests.yml`
2. Push to repository

The workflow:
- Runs on pushes to `main`/`develop` affecting `backend/`
- Uses `continue-on-error: true` (E2E failures don't block merge)
- Caches uv dependencies for speed

### Local CI Simulation

```bash
# Run what CI runs
make test-all
```

---

## Best Practices

### DO

- Use unique emails per test: `f"test-{uuid4().hex[:8]}@example.com"`
- Mark tests with appropriate markers: `@pytest.mark.e2e`
- Keep E2E tests focused on critical workflows
- Use `e2e_client` fixture for most tests

### DON'T

- Share state between tests (each test gets fresh tables)
- Test every endpoint in E2E (use unit tests for edge cases)
- Skip the `IS_TEST=True` environment variable
- Run E2E tests without Docker

---

## Further Reading

- [Testcontainers Documentation](https://testcontainers.com/guides/getting-started-with-testcontainers-for-python/)
- [Schemathesis Documentation](https://schemathesis.readthedocs.io/)
- [pytest-asyncio Documentation](https://pytest-asyncio.readthedocs.io/)
