Skip to content

Testing

Running Tests

cd appmecca-api

# All tests with coverage
make test

# Unit tests only
make test-unit

Test Organization

tests/
├── conftest.py              # Shared fixtures (mock pool, async client, settings)
├── unit/                    # ~230 modules, one per feature area
│   ├── test_health.py       # Health endpoint tests
│   ├── test_config.py       # Configuration tests
│   ├── test_auth.py         # API-key authentication tests
│   ├── test_connection.py   # DB pool management tests
│   ├── test_repository_base.py  # Base repository tests
│   ├── auth/                # Sessions, identity, scopes, rate limits, deps
│   ├── email/               # Providers, link base, render
│   └── tasks/               # Celery task tests
└── integration/             # Reserved; the real-PG suites live in the platform repo

pyproject.toml declares asyncio_mode = "auto", so async def tests need no @pytest.mark.asyncio decorator — the examples below omit it. Two markers are registered: unit and integration.

Coverage

Coverage target: 80% minimum (fail_under = 80 in pyproject.toml).

Testing Approach

No Real Database in Unit Tests

Unit tests mock the asyncpg pool. The conftest.py provides:

  • mock_pool — A mock asyncpg pool with acquire() as an async context manager
  • mock_conn — The mock connection returned by pool.acquire()
  • async_client — An HTTPX async client wired to the FastAPI app with mocked DB

Example: Testing a Repository

async def test_list_apps(mock_pool, mock_conn):
    """List apps queries the database."""
    mock_conn.fetch.return_value = [
        {"app_name": "myapp", "branch": "main"},
    ]
    repo = AppRepository(mock_pool)
    apps = await repo.list_apps(group_id=None)
    assert len(apps) == 1
    assert apps[0]["app_name"] == "myapp"

Example: Testing an Endpoint

async def test_list_apps_endpoint(async_client, mock_conn):
    """GET /api/apps returns app list."""
    # Set up auth to succeed
    mock_conn.fetchrow.return_value = {
        "username": "admin",
        "scope": "admin",
        "key_prefix": "am_k1_test",
    }
    mock_conn.fetch.return_value = []

    response = await async_client.get(
        "/api/apps",
        headers={"Authorization": "Bearer am_k1_test_key_123"},
    )
    assert response.status_code == 200

No ?environment= parameter

It was removed from the whole API surface and the schema (migration 010). A test that still sends it is asserting against a shape that no longer exists.

Example: Testing Auth

async def test_invalid_key_returns_401(async_client, mock_conn):
    """Invalid API key returns 401."""
    mock_conn.fetchrow.return_value = None  # Key not found

    response = await async_client.get(
        "/api/apps",
        headers={"Authorization": "Bearer am_k1_bad_key"},
    )
    assert response.status_code == 401

Fixtures

Fixture Scope Description
_reset_db_pool function (autouse) Resets the global pool before/after each test
_reset_auth_rate_limiter function (autouse) Clears slowapi limiter state so limits don't leak between tests
_reset_email_module_state function (autouse) Resets email provider module state
test_settings function Settings with test values
mock_pool function Mock asyncpg pool
mock_conn function Mock connection from mock_pool.acquire()
app_repo / build_repo / deploy_repo / history_repo function Repositories pre-wired to mock_pool
async_client function HTTPX async client with mocked DB

For routes gated by a FastAPI dependency, prefer app.dependency_overrides over patching module attributes — a gate resolved outside Depends is invisible to overrides and therefore untestable.

Integration Tests

appmecca-api/tests/integration/ is reserved but currently empty. The real-PostgreSQL suites for this API live in the platform repo under tests/integration/, driven by docker-compose.auth-flow.yml:

# from the platform repo
make test-migrations        # full alembic chain against postgres:16
make test-api-integration   # boot PG + Redis + API, smoke /api/health
make test-agent-integration # PGQueueClient claim/transition against real PG

make check and scripts/run-ci.sh sweep the whole tests/integration/ directory, so a new integration test is covered the day it is written.

A fake pool cannot catch a widened SQL predicate

Mocks honour exactly what they are told to honour, and a string assertion on SQL cannot distinguish a correct predicate from a wider one. Scoping changes — anything touching group_id, branch or a tenant filter — need a real-PostgreSQL test alongside the unit test. Deleting the real-PG half leaves a cross-tenant widening with no coverage anywhere.

Unit tests must never reach a live environment

Constructing a real client, or leaving APPMECCA_API_URL / APPMECCA_API_KEY / APPMECCA_DATABASE_URL set, has previously let unit tests write to a deployed environment. Patch the HTTP client layer fully, and match mocks to the real contract — AppMeccaClient.put() raises on non-2xx, so simulate failure with side_effect=APIError(...), never a return_value carrying a status code.