Skip to content

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.

See Also