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.
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_tokencookie (then the CSRF header is checked).
It does not read from Authorization: Bearer. Use the body form in tests.
Cookie delivery 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=Falseonly when you specifically want to test stateless refresh (no revocation/replay). MemoryTokenAdapterauto-prunes expired rows on read, so tests do not need to travel through time for one-time tokens.TestClienttriggers the lifespan (including anyinitialize_jwks/initialize_rolesyou wired) automatically when used as a context manager.