Skip to content

Guide: Testing

Use the in-memory adapters for fast, hermetic tests. No DB required.

pytest config

The workspace pyproject.toml sets asyncio_mode = "auto" and testpaths = ["tests"]. With auto mode you do not need @pytest.mark.asyncio on every test.

[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]

A minimal auth fixture

import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient

from fastauth import FastAuth, FastAuthConfig
from fastauth.adapters.memory import (
    MemoryRoleAdapter,
    MemoryTokenAdapter,
    MemoryUserAdapter,
)
from fastauth.providers.credentials import CredentialsProvider


@pytest.fixture
def app_and_client():
    config = FastAuthConfig(
        secret="x" * 32,
        providers=[CredentialsProvider()],
        adapter=MemoryUserAdapter(),
        token_adapter=MemoryTokenAdapter(),
        base_url="http://testserver",
    )
    auth = FastAuth(config)
    auth.role_adapter = MemoryRoleAdapter()
    app = FastAPI()
    auth.mount(app)
    with TestClient(app) as client:
        yield app, client


def test_register_login_refresh(app_and_client):
    _, c = app_and_client

    r = c.post("/auth/register", json={"email": "a@b.com", "password": "s3cur3-pw!"})
    assert r.status_code == 201
    refresh = r.json()["refresh_token"]

    r = c.post("/auth/refresh", json={"refresh_token": refresh})
    assert r.status_code == 200
    access = r.json()["access_token"]

    r = c.get("/auth/me", headers={"Authorization": f"Bearer {access}"})
    assert r.status_code == 200
    assert r.json()["email"] == "a@b.com"

Refresh convention

POST /auth/refresh reads the refresh token from:

  • the JSON body {"refresh_token": "..."} (no CSRF), or
  • the refresh_token cookie (then the CSRF header is checked).

It does not read from Authorization: Bearer. Use the body form in tests.

TestClient as a context manager keeps cookies between requests:

def test_cookie_mode():
    config = FastAuthConfig(
        secret="x" * 32,
        providers=[CredentialsProvider()],
        adapter=MemoryUserAdapter(),
        token_adapter=MemoryTokenAdapter(),
        token_delivery="cookie",
    )
    auth = FastAuth(config)
    app = FastAPI()
    auth.mount(app)
    with TestClient(app) as c:
        c.post("/auth/login", json={"email": "a@b.com", "password": "s3cur3-pw!"})
        csrf = c.cookies["csrf_token"]
        # unsafe (POST) requests need the header:
        r = c.post("/api/thing", json={"x": 1}, headers={"X-CSRF-Token": csrf})
        assert r.status_code == 200

RBAC test

def test_rbac_requires_role():
    config = FastAuthConfig(
        secret="x" * 32,
        providers=[CredentialsProvider()],
        adapter=MemoryUserAdapter(),
        token_adapter=MemoryTokenAdapter(),
        roles=[{"name": "admin", "permissions": ["users:read"]}],
        default_role="user",
    )
    auth = FastAuth(config)
    auth.role_adapter = MemoryRoleAdapter()
    app = FastAPI()
    auth.mount(app)
    # Seed roles:
    import asyncio
    asyncio.run(auth.initialize_roles())

    with TestClient(app) as c:
        c.post("/auth/register", json={"email": "a@b.com", "password": "s3cur3-pw!"})
        # /auth/roles/me shows the "user" default role
        print(c.get("/auth/roles/me").json())

Tips

  • Use require_token_adapter_for_refresh=False only when you specifically want to test stateless refresh (no revocation/replay).
  • MemoryTokenAdapter auto-prunes expired rows on read, so tests do not need to travel through time for one-time tokens.
  • TestClient triggers the lifespan (including any initialize_jwks / initialize_roles you wired) automatically when used as a context manager.