Skip to content

Testing

Test client for in-memory testing of Xitzin applications.

TestClient

Make requests to your application without running a server.

TestClient

TestClient(app: 'Xitzin')

Test client for Xitzin applications.

Allows testing handlers without running a real Gemini server.

Example

app = Xitzin()

@app.gemini("/") def home(request: Request): return "# Welcome"

client = TestClient(app) response = client.get("/") assert response.status == 20 assert "Welcome" in response.body

Create a test client for an application.

Parameters:

Name Type Description Default
app 'Xitzin'

The Xitzin application to test.

required
Source code in src/xitzin/testing.py
def __init__(self, app: "Xitzin") -> None:
    """Create a test client for an application.

    Args:
        app: The Xitzin application to test.
    """
    self._app = app
    self._default_fingerprint: str | None = None

delete

delete(
    path: str,
    *,
    token: str | None = None,
    cert_fingerprint: str | None = None,
) -> TestResponse

Make a Titan delete request (zero-byte upload).

Parameters:

Name Type Description Default
path str

The request path to delete.

required
token str | None

Authentication token (if required by route).

None
cert_fingerprint str | None

Mock client certificate fingerprint.

None

Returns:

Type Description
TestResponse

TestResponse with status, meta, and body.

Example

response = client.delete("/files/old.gmi", token="secret123") assert response.is_success

Source code in src/xitzin/testing.py
def delete(
    self,
    path: str,
    *,
    token: str | None = None,
    cert_fingerprint: str | None = None,
) -> TestResponse:
    """Make a Titan delete request (zero-byte upload).

    Args:
        path: The request path to delete.
        token: Authentication token (if required by route).
        cert_fingerprint: Mock client certificate fingerprint.

    Returns:
        TestResponse with status, meta, and body.

    Example:
        response = client.delete("/files/old.gmi", token="secret123")
        assert response.is_success
    """
    return self.upload(
        path,
        b"",
        mime_type="text/gemini",
        token=token,
        cert_fingerprint=cert_fingerprint,
    )

get

get(
    path: str,
    *,
    query: str | None = None,
    cert_fingerprint: str | None = None,
) -> TestResponse

Make a test request.

Parameters:

Name Type Description Default
path str

The request path (e.g., "/user/alice").

required
query str | None

Optional query string (for input responses).

None
cert_fingerprint str | None

Mock client certificate fingerprint.

None

Returns:

Type Description
TestResponse

TestResponse with status, meta, and body.

Example

response = client.get("/") assert response.is_success

Source code in src/xitzin/testing.py
def get(
    self,
    path: str,
    *,
    query: str | None = None,
    cert_fingerprint: str | None = None,
) -> TestResponse:
    """Make a test request.

    Args:
        path: The request path (e.g., "/user/alice").
        query: Optional query string (for input responses).
        cert_fingerprint: Mock client certificate fingerprint.

    Returns:
        TestResponse with status, meta, and body.

    Example:
        response = client.get("/")
        assert response.is_success
    """
    # Build URL
    url = f"gemini://testserver{path}"
    if query:
        url += f"?{quote_plus(query)}"

    # Create mock GeminiRequest
    request = GeminiRequest.from_line(url)

    # Set certificate info
    fingerprint = cert_fingerprint or self._default_fingerprint
    if fingerprint:
        request.client_cert_fingerprint = fingerprint

    # Handle request through the app
    response = self._handle_sync(request)

    # Convert bytes body to str for TestResponse
    body = response.body
    if isinstance(body, bytes):
        body = body.decode("utf-8")

    return TestResponse(
        status=response.status,
        meta=response.meta,
        body=body,
    )

get_input

get_input(
    path: str,
    input_value: str,
    *,
    cert_fingerprint: str | None = None,
) -> TestResponse

Make a request with an input value.

Simulates a user responding to a status 10/11 input prompt.

Parameters:

Name Type Description Default
path str

The request path.

required
input_value str

The user's input (will be URL-encoded).

required
cert_fingerprint str | None

Mock client certificate fingerprint.

None

Returns:

Type Description
TestResponse

TestResponse from the handler.

Example

First request gets input prompt

response = client.get("/search") assert response.is_input_required

Second request with input value

response = client.get_input("/search", "hello world") assert response.is_success

Source code in src/xitzin/testing.py
def get_input(
    self,
    path: str,
    input_value: str,
    *,
    cert_fingerprint: str | None = None,
) -> TestResponse:
    """Make a request with an input value.

    Simulates a user responding to a status 10/11 input prompt.

    Args:
        path: The request path.
        input_value: The user's input (will be URL-encoded).
        cert_fingerprint: Mock client certificate fingerprint.

    Returns:
        TestResponse from the handler.

    Example:
        # First request gets input prompt
        response = client.get("/search")
        assert response.is_input_required

        # Second request with input value
        response = client.get_input("/search", "hello world")
        assert response.is_success
    """
    return self.get(path, query=input_value, cert_fingerprint=cert_fingerprint)

upload

upload(
    path: str,
    content: bytes | str,
    *,
    mime_type: str = "text/gemini",
    token: str | None = None,
    cert_fingerprint: str | None = None,
) -> TestResponse

Make a Titan upload request.

Parameters:

Name Type Description Default
path str

The request path (e.g., "/upload/file.gmi").

required
content bytes | str

Content to upload (str will be UTF-8 encoded).

required
mime_type str

Content MIME type (default: text/gemini).

'text/gemini'
token str | None

Authentication token (if required by route).

None
cert_fingerprint str | None

Mock client certificate fingerprint.

None

Returns:

Type Description
TestResponse

TestResponse with status, meta, and body.

Example

response = client.upload( "/files/test.gmi", "# Hello World", mime_type="text/gemini", token="secret123" ) assert response.is_success

Source code in src/xitzin/testing.py
def upload(
    self,
    path: str,
    content: bytes | str,
    *,
    mime_type: str = "text/gemini",
    token: str | None = None,
    cert_fingerprint: str | None = None,
) -> TestResponse:
    """Make a Titan upload request.

    Args:
        path: The request path (e.g., "/upload/file.gmi").
        content: Content to upload (str will be UTF-8 encoded).
        mime_type: Content MIME type (default: text/gemini).
        token: Authentication token (if required by route).
        cert_fingerprint: Mock client certificate fingerprint.

    Returns:
        TestResponse with status, meta, and body.

    Example:
        response = client.upload(
            "/files/test.gmi",
            "# Hello World",
            mime_type="text/gemini",
            token="secret123"
        )
        assert response.is_success
    """
    # Convert str to bytes
    if isinstance(content, str):
        content_bytes = content.encode("utf-8")
    else:
        content_bytes = content

    # Build Titan URL
    size = len(content_bytes)
    url = f"titan://testserver{path};size={size};mime={mime_type}"
    if token:
        url += f";token={token}"

    # Create TitanRequest
    request = NauyacaTitanRequest.from_line(url)
    request.content = content_bytes

    # Set certificate info
    fingerprint = cert_fingerprint or self._default_fingerprint
    if fingerprint:
        request.client_cert_fingerprint = fingerprint

    # Handle through app
    response = self._handle_titan_sync(request)

    # Convert bytes body to str for TestResponse
    body = response.body
    if isinstance(body, bytes):
        body = body.decode("utf-8")

    return TestResponse(
        status=response.status,
        meta=response.meta,
        body=body,
    )

with_certificate

with_certificate(fingerprint: str) -> 'TestClient'

Create a new client with a default certificate fingerprint.

Parameters:

Name Type Description Default
fingerprint str

The certificate fingerprint to use for all requests.

required

Returns:

Type Description
'TestClient'

A new TestClient with the default fingerprint set.

Example

Create authenticated client

auth_client = client.with_certificate("abc123...")

All requests from this client include the certificate

response = auth_client.get("/admin") assert response.is_success

Source code in src/xitzin/testing.py
def with_certificate(self, fingerprint: str) -> "TestClient":
    """Create a new client with a default certificate fingerprint.

    Args:
        fingerprint: The certificate fingerprint to use for all requests.

    Returns:
        A new TestClient with the default fingerprint set.

    Example:
        # Create authenticated client
        auth_client = client.with_certificate("abc123...")

        # All requests from this client include the certificate
        response = auth_client.get("/admin")
        assert response.is_success
    """
    new_client = TestClient(self._app)
    new_client._default_fingerprint = fingerprint
    return new_client

TestResponse

Response object returned by the test client.

TestResponse dataclass

TestResponse(status: int, meta: str, body: str | None)

Response from the test client.

Provides convenient access to response data and status checking methods.

body instance-attribute

body: str | None

The response body (only present for 2x success responses).

input_prompt property

input_prompt: str | None

Get the input prompt if input is required.

is_certificate_required property

is_certificate_required: bool

Check if a client certificate is required (6x).

is_error property

is_error: bool

Check if this is an error response (4x, 5x, 6x).

is_input_required property

is_input_required: bool

Check if input is required (1x).

is_redirect property

is_redirect: bool

Check if this is a redirect (3x).

is_success property

is_success: bool

Check if this is a success response (2x).

meta instance-attribute

meta: str

The meta field (MIME type, prompt, redirect URL, or error message).

mime_type property

mime_type: str | None

Get the MIME type if this is a success response.

redirect_url property

redirect_url: str | None

Get the redirect URL if this is a redirect response.

status instance-attribute

status: int

The status code (10-62).

Context Manager

test_app

Context manager that runs startup and shutdown handlers.

test_app

test_app(
    app: "Xitzin",
) -> Generator[TestClient, None, None]

Context manager that runs the app's lifespan for testing.

This runs startup handlers before yielding and shutdown handlers when the context exits.

Parameters:

Name Type Description Default
app 'Xitzin'

The Xitzin application to test.

required

Yields:

Type Description
TestClient

A TestClient bound to the application.

Example

app = Xitzin()

@app.on_startup async def startup(): app.state.db = await connect_db()

@app.on_shutdown async def shutdown(): await app.state.db.close()

with test_app(app) as client: # Startup has run, db is connected response = client.get("/") assert response.is_success

Shutdown has run, db is closed

Source code in src/xitzin/testing.py
@contextmanager
def test_app(app: "Xitzin") -> Generator[TestClient, None, None]:
    """Context manager that runs the app's lifespan for testing.

    This runs startup handlers before yielding and shutdown handlers
    when the context exits.

    Args:
        app: The Xitzin application to test.

    Yields:
        A TestClient bound to the application.

    Example:
        app = Xitzin()

        @app.on_startup
        async def startup():
            app.state.db = await connect_db()

        @app.on_shutdown
        async def shutdown():
            await app.state.db.close()

        with test_app(app) as client:
            # Startup has run, db is connected
            response = client.get("/")
            assert response.is_success
        # Shutdown has run, db is closed
    """
    try:
        loop = asyncio.get_event_loop()
    except RuntimeError:
        loop = asyncio.new_event_loop()
        asyncio.set_event_loop(loop)

    # Run startup
    loop.run_until_complete(app._run_startup())

    try:
        yield TestClient(app)
    finally:
        # Run shutdown
        loop.run_until_complete(app._run_shutdown())