How-To: Test Authorization¶
This guide shows how to test routes protected by fastapi-topaz.
Using dependency_overrides¶
FastAPI's built-in mechanism for mocking dependencies:
from fastapi.testclient import TestClient
from myapp.main import app
from myapp.auth import topaz_config
from fastapi_topaz import require_policy_allowed
# Create a mock that always allows
def mock_always_allow():
return None
# Override the dependency
app.dependency_overrides[
require_policy_allowed(topaz_config, "myapp.GET.documents")
] = mock_always_allow
# Test passes without Topaz running
client = TestClient(app)
response = client.get("/documents")
assert response.status_code == 200
# Clean up
app.dependency_overrides.clear()
Testing Denied Access¶
from fastapi import HTTPException
def mock_always_deny():
raise HTTPException(status_code=403, detail="Access denied")
app.dependency_overrides[
require_policy_allowed(topaz_config, "myapp.GET.documents")
] = mock_always_deny
response = client.get("/documents")
assert response.status_code == 403
Pytest Fixtures¶
Create reusable fixtures:
import pytest
from fastapi.testclient import TestClient
from fastapi import HTTPException
from myapp.main import app
from myapp.auth import topaz_config
from fastapi_topaz import require_policy_allowed, require_rebac_allowed
@pytest.fixture
def client():
"""Test client with no auth overrides."""
return TestClient(app)
@pytest.fixture
def authed_client():
"""Test client that bypasses all authorization."""
# Store all dependencies to override
deps_to_mock = [
require_policy_allowed(topaz_config, "myapp.GET.documents"),
require_policy_allowed(topaz_config, "myapp.POST.documents"),
require_rebac_allowed(topaz_config, "document", "can_read"),
require_rebac_allowed(topaz_config, "document", "can_write"),
]
for dep in deps_to_mock:
app.dependency_overrides[dep] = lambda: None
yield TestClient(app)
app.dependency_overrides.clear()
@pytest.fixture
def denied_client():
"""Test client that denies all authorization."""
def deny():
raise HTTPException(status_code=403, detail="Access denied")
deps_to_mock = [
require_policy_allowed(topaz_config, "myapp.GET.documents"),
require_rebac_allowed(topaz_config, "document", "can_read"),
]
for dep in deps_to_mock:
app.dependency_overrides[dep] = deny
yield TestClient(app)
app.dependency_overrides.clear()
# Usage in tests
def test_list_documents_when_authorized(authed_client):
response = authed_client.get("/documents")
assert response.status_code == 200
def test_list_documents_when_denied(denied_client):
response = denied_client.get("/documents")
assert response.status_code == 403
Asserting Which Policies Were Checked¶
To check which policies a request evaluated, record decisions with MockTopazConfig(record_decisions=True) and install it on your real config (see install_mock below):
from fastapi_topaz.testing import MockTopazConfig, install_mock
def test_list_checks_policy(monkeypatch, client):
mock = MockTopazConfig(record_decisions=True, identity_returns="alice")
install_mock(monkeypatch, mock, topaz_config)
assert client.get("/documents").status_code == 200
assert [d.policy_path for d in mock.decisions] == ["myapp.GET.documents"]
def test_list_denied(monkeypatch, client):
install_mock(monkeypatch, MockTopazConfig(default_decision=False), topaz_config)
assert client.get("/documents").status_code == 403
Integration Testing with Topaz¶
For integration tests with a real Topaz instance:
import pytest
import subprocess
import time
@pytest.fixture(scope="session")
def topaz_instance():
"""Start Topaz container for integration tests."""
# Start Topaz
subprocess.run([
"docker", "run", "-d",
"--name", "topaz-test",
"-p", "8282:8282",
"ghcr.io/aserto-dev/topaz:latest",
"run"
], check=True)
# Wait for it to be ready
time.sleep(5)
yield
# Cleanup
subprocess.run(["docker", "rm", "-f", "topaz-test"])
def test_real_authorization(topaz_instance, client):
"""Test against real Topaz instance."""
response = client.get("/documents", headers={"X-User-ID": "alice"})
# Result depends on your actual policies
assert response.status_code in [200, 403]
Mocking a Real Config with install_mock¶
install_mock patches a real TopazConfig so the middleware and dependencies run unchanged while decisions come from a MockTopazConfig:
from fastapi_topaz.testing import MockTopazConfig, install_mock, when_policy
def test_reader_is_denied(monkeypatch, client):
mock = MockTopazConfig(
rules=[when_policy("myapp.DELETE.*").deny()],
identity_returns="alice",
)
install_mock(monkeypatch, mock, topaz_config)
assert client.delete("/documents/1").status_code == 403
It patches check_decision, the batched path used by check_relations(batch=True), and identity_provider. The config's own identity provider is replaced: a string identity_returns becomes an IDENTITY_TYPE_SUB identity with that value, and identity_returns=None gives an unauthenticated identity (IDENTITY_TYPE_NONE), so the middleware takes its 401 or anonymous path. To test your identity provider, call it directly instead of through install_mock.
Testing Resource Context¶
Verify the correct context is passed to policies:
def test_resource_context_includes_path_params(mock_authorizer):
client.get("/documents/123", headers={"X-User-ID": "alice"})
# Check what was passed to the authorizer
call_args = mock_authorizer.decisions.call_args
resource_context = call_args.kwargs.get("resource_context", {})
assert resource_context.get("id") == "123"
assert resource_context.get("object_type") == "document"
Test Structure¶
Recommended structure for projects with authorization:
tests/
unit/ # Fast, mocked tests
test_config.py
test_auth.py
test_models.py
integration/ # Full stack tests
conftest.py
test_scenarios.py
| Test Type | Speed | Services | Purpose |
|---|---|---|---|
| Unit | < 1s | None | Component testing |
| Integration | ~10s | All (Docker) | Workflow testing |
Run unit tests for fast feedback during development. Run integration tests before commits.