Testing¶
Running Tests¶
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 withacquire()as an async context managermock_conn— The mock connection returned bypool.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.