Guide: Testing¶
FastAuth provides an in-memory adapter that makes it easy to write fast, database-free tests for your application's auth flows.
Setup¶
Install test dependencies:
Using the Memory adapter¶
MemoryUserAdapter, MemoryTokenAdapter, MemoryRoleAdapter, and MemoryOAuthAccountAdapter store everything in Python dicts — no database required.
tests/conftest.py
import pytest
from fastapi import FastAPI
from httpx import ASGITransport, AsyncClient
from fastauth import FastAuth, FastAuthConfig
from fastauth.adapters.memory import (
MemoryTokenAdapter,
MemoryUserAdapter,
)
from fastauth.email_transports.console import ConsoleTransport
from fastauth.providers.credentials import CredentialsProvider
def make_app() -> FastAPI:
user_adapter = MemoryUserAdapter()
token_adapter = MemoryTokenAdapter()
config = FastAuthConfig(
secret="test-secret-at-least-32-characters-long",
providers=[CredentialsProvider()],
adapter=user_adapter,
token_adapter=token_adapter,
email_transport=ConsoleTransport(),
base_url="http://testserver",
)
auth = FastAuth(config)
app = FastAPI()
auth.mount(app)
return app
@pytest.fixture
def app():
return make_app()
@pytest.fixture
async def client(app):
async with AsyncClient(
transport=ASGITransport(app=app), base_url="http://testserver"
) as c:
yield c
Writing tests¶
Test registration and login¶
tests/test_auth.py
import pytest
@pytest.mark.asyncio
async def test_register_and_login(client):
# Register
res = await client.post("/auth/register", json={
"email": "alice@example.com",
"password": "s3cur3P@ss!",
})
assert res.status_code == 201
assert "access_token" in res.json()
assert res.json()["token_type"] == "bearer"
# Login
res = await client.post("/auth/login", json={
"email": "alice@example.com",
"password": "s3cur3P@ss!",
})
assert res.status_code == 200
tokens = res.json()
assert "access_token" in tokens
assert "refresh_token" in tokens
@pytest.mark.asyncio
async def test_wrong_password(client):
await client.post("/auth/register", json={
"email": "bob@example.com",
"password": "correct-pass",
})
res = await client.post("/auth/login", json={
"email": "bob@example.com",
"password": "wrong-pass",
})
assert res.status_code == 401
@pytest.mark.asyncio
async def test_duplicate_email(client):
payload = {"email": "carol@example.com", "password": "pass"}
await client.post("/auth/register", json=payload)
res = await client.post("/auth/register", json=payload)
assert res.status_code == 409
Test protected routes¶
@pytest.mark.asyncio
async def test_protected_route(client, app):
from fastapi import Depends
from fastauth.api.deps import require_auth
@app.get("/me-test")
async def me_test(user=Depends(require_auth)):
return {"email": user["email"]}
# Without token → 401
res = await client.get("/me-test")
assert res.status_code == 401
# Register and get token
await client.post("/auth/register", json={
"email": "dave@example.com", "password": "pass"
})
login = await client.post("/auth/login", json={
"email": "dave@example.com", "password": "pass"
})
token = login.json()["access_token"]
# With token → 200
res = await client.get("/me-test", headers={"Authorization": f"Bearer {token}"})
assert res.status_code == 200
assert res.json()["email"] == "dave@example.com"
Test token refresh¶
@pytest.mark.asyncio
async def test_token_refresh(client):
await client.post("/auth/register", json={
"email": "eve@example.com", "password": "pass"
})
login = await client.post("/auth/login", json={
"email": "eve@example.com", "password": "pass"
})
refresh_token = login.json()["refresh_token"]
res = await client.post("/auth/refresh", json={"refresh_token": refresh_token})
assert res.status_code == 200
assert "access_token" in res.json()
Test with RBAC¶
tests/test_rbac.py
import pytest
from fastapi import FastAPI
from httpx import ASGITransport, AsyncClient
from fastauth import FastAuth, FastAuthConfig
from fastauth.adapters.memory import MemoryRoleAdapter, MemoryTokenAdapter, MemoryUserAdapter
from fastauth.api.deps import require_role
from fastauth.providers.credentials import CredentialsProvider
@pytest.fixture
async def rbac_client():
user_adapter = MemoryUserAdapter()
token_adapter = MemoryTokenAdapter()
role_adapter = MemoryRoleAdapter()
config = FastAuthConfig(
secret="test-secret-at-least-32-characters-long",
providers=[CredentialsProvider()],
adapter=user_adapter,
token_adapter=token_adapter,
roles=[{"name": "admin", "permissions": ["users:delete"]}],
)
auth = FastAuth(config)
auth.role_adapter = role_adapter
app = FastAPI()
auth.mount(app)
@app.get("/admin-only")
async def admin_only(user=Depends(require_role("admin"))):
return {"ok": True}
async with AsyncClient(
transport=ASGITransport(app=app), base_url="http://testserver"
) as client:
yield client, auth
@pytest.mark.asyncio
async def test_role_required(rbac_client):
client, auth = rbac_client
# Register a user
res = await client.post("/auth/register", json={
"email": "frank@example.com", "password": "pass"
})
token = res.json()["access_token"]
# Without admin role → 403
res = await client.get("/admin-only", headers={"Authorization": f"Bearer {token}"})
assert res.status_code == 403
Tips¶
- Isolate state per test — create a fresh
make_app()in each test or fixture to avoid state leaking between tests. - Use
pytest-asynciowithasyncio_mode = "auto"inpytest.inito avoid boilerplate@pytest.mark.asynciodecorators. - Check the
ConsoleTransportoutput — email verification tokens are printed to stdout during tests. Capture them withcapsysif needed. - Switch to a real DB for integration tests — swap
MemoryUserAdapterforSQLAlchemyAdapterwith an in-memory SQLite URL (sqlite+aiosqlite:///:memory:).