Test native routes#

This guide shows you how to test native routes against a real Plone site, with pytest and pytest-plone.

Install the test layer#

Add the testing extra to your test dependencies:

uv add --group test "ploneasgi[testing] @ git+https://github.com/simplesconsultoria/ploneasgi" pytest-plone

Use the functional layer#

A native route reaches Plone from a worker thread, on a second ZODB connection. That connection sees only what has been committed, so use FUNCTIONAL_TESTING, and commit in the fixture:

from ploneasgi import context
from ploneasgi.testing import FUNCTIONAL_TESTING
from pytest_plone import fixtures_factory

import pytest
import transaction


globals().update(fixtures_factory(((FUNCTIONAL_TESTING, "functional"),)))


@pytest.fixture
def portal(functional_portal):
    transaction.commit()
    yield functional_portal


@pytest.fixture
def layer_db(portal, monkeypatch):
    monkeypatch.setattr(context, "default_db", lambda: portal._p_jar.db())

layer_db points run_authenticated() at the test database. Without it, the worker opens Zope2.DB, which is not the layer's.

To base your own layer on ploneasgi's, use PLONEASGI_FIXTURE in its defaultBases.

Call the route#

Build the route from a registration and call its endpoint with a raw ASGI scope:

from ploneasgi.zcml import ASGIRoute
from ploneasgi.zcml import build_route
from starlette.requests import Request

import asyncio


async def receive():
    return {"type": "http.request", "body": b""}


def scope(path, headers=()):
    return {
        "type": "http",
        "http_version": "1.1",
        "method": "GET",
        "scheme": "http",
        "path": path,
        "raw_path": path.encode(),
        "query_string": b"",
        "root_path": "",
        "headers": list(headers),
        "server": ("127.0.0.1", 8080),
        "client": ("127.0.0.1", 50000),
    }


@pytest.mark.usefixtures("layer_db")
def test_anonymous_is_refused(portal):
    route = ASGIRoute(
        path="/status",
        handler=status,
        permission="cmf.ManagePortal",
        site=portal.getId(),
    )
    endpoint = build_route(route).endpoint
    response = asyncio.run(endpoint(Request(scope("/++asgi++/status"), receive)))
    assert response.status_code == 403

The endpoint reads the request body before it calls the handler, so the Request needs a receive; this one delivers an empty body. Send a Basic authorization header in headers to test as a user.

Test both sides of a permission#

A guard that refuses everyone passes every test that expects a refusal. For each route, test a user who must pass, and a user who must not.

Check against a running server#

Unit tests do not cover everything a running server does. Before you rely on a route that touches authentication or traversal, request it from a running instance with real credentials.