Skip to content
Back to skills

Hex Test Restapi Endpoint

ASecurity

Use when testing one REST endpoint through the real ASGI app — one file per endpoint under `tests/integration/api/<resource>/`, asserting the success body and each advertised error code, with per-resource fixtures in a sibling `conftest.py`. Not properties discovered across all routes at once (`hex-test-app-invariants`), not the auth half — a token verifier's unit test, 401/403, role rejection, cross-tenant 404 (`hex-test-restapi-auth`) — and not the fixtures themselves (`hex-test-integration...

  • 2 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added October 14, 2026
developmentpythonrustsqlfastapitestinggitapi

Works with

  • cli
  • api

Security analysis

A100/100

Scanned October 14, 2026

npx -y skills add wntic/pyhouse --skill hex-test-restapi-endpoint --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Hex Test Restapi Endpoint?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Hex Test Restapi Endpoint
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/wntic-hex-test-restapi-endpoint/badge)](https://www.skillsdirectory.com/skills/wntic-hex-test-restapi-endpoint)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: hex-test-restapi-endpoint
description: Use when testing one REST endpoint through the real ASGI app — one file per endpoint under `tests/integration/api/<resource>/`, asserting the success body and each advertised error code, with per-resource fixtures in a sibling `conftest.py`. Not properties discovered across all routes at once (`hex-test-app-invariants`), not the auth half — a token verifier's unit test, 401/403, role rejection, cross-tenant 404 (`hex-test-restapi-auth`) — and not the fixtures themselves (`hex-test-integration-setup`).
---

# Hex Test — REST API Endpoint

Consult `test-principles` for the testing constitution. Where this skill contradicts `test-principles`, the constitution wins.

Produces one integration-test file per endpoint. Self-contained: every test in the file constructs its own state by calling factory fixtures or POSTing through the API; no cross-test state, no shared registries, no cross-file edits when a new endpoint is added.

## When to use vs. neighbours

- A new or modified endpoint added by `hex-restapi-endpoint` → this skill.
- A new resource introduces several endpoints (create + list + get + update + delete) → invoke this skill once per endpoint file; sibling files share a per-resource `conftest.py`.
- The `tests/integration/conftest.py` itself (rollback, container fixtures, `container`, `real_app`) → `hex-test-integration-setup` (one-shot).
- The auth half — the `authenticated_client` factory and its signing-key fixtures, a route driven as an authenticated caller, a role rejection or a cross-tenant 404, the token verifier's own unit test → `hex-test-restapi-auth` (auth apps only; this skill is complete without it).
- The route-side auth dependency, the role gate and the 401/403 codes a route advertises because of them → `hex-restapi-auth`.
- Cross-cutting "every advertised error code carries the catalogue's shape" / CORS / request-size → `hex-test-app-invariants` (one-shot; discovers by walking the app's published document).
- Repository contract (real DB, no HTTP) → `hex-test-repository-contract`.
- Pure domain unit test → `hex-test-domain`.
- The testing constitution these rules defer to — speed targets, fixture placement, the mocking prohibition → `test-principles`.
- A structural invariant enforced by a grep firewall at compile time rather than over HTTP → `test-architecture-rule`.

## Template(s) — pytest, httpx2 over an in-process ASGI transport, FastAPI

```
tests/integration/api/<resource>/
├── conftest.py                                  # per-resource fixtures (sibling-shared)
└── test_<verb>_<noun>.py                        # one file per endpoint
```

**The templates below are the primary, auth-free form** — a public route, or any route in an app that
declares no auth. An authenticated route is the same file driven through an authenticated client
instead of the plain one, and `hex-test-restapi-auth` carries those variants along with the role and
tenancy assertions that only exist when there is a caller to have a role.

### `test_<verb>_<noun>.py` — the endpoint test

```python
from fastapi import FastAPI
from httpx2 import ASGITransport, AsyncClient

from myapp.domain.exceptions import ValidationError


async def test_create_foo_returns_the_created_foo(real_app: FastAPI) -> None:
    transport = ASGITransport(app=real_app)
    async with AsyncClient(transport=transport, base_url="http://testserver") as client:
        response = await client.post("/foos", json={"name": "alpha"})

    assert response.status_code == 201
    assert response.json()["name"] == "alpha"


async def test_create_foo_empty_body_returns_422(real_app: FastAPI) -> None:
    transport = ASGITransport(app=real_app)
    async with AsyncClient(transport=transport, base_url="http://testserver") as client:
        response = await client.post("/foos", json={})

    assert response.status_code == 422
    assert response.json()["code"] == ValidationError.code
```

The plain `AsyncClient` over `real_app` is the sanctioned client for a route with no auth dependency
(`hex-test-restapi-auth` rule 4). Every error path for the same endpoint lives in this file too, each
asserted by `code` (Rule 8) — here the `422` an empty create body produces. A `404` on an `{id}` route,
and a `409` where the aggregate carries a uniqueness constraint, are further tests of the same shape.
Where the route transfers a file, the test also asserts the media type and disposition the route sets,
never a fixed download shape.

### Per-resource `conftest.py` — relational seed factory, SQLAlchemy

A raw `INSERT` is legitimate setup here: `hex-test-repository-contract` rule 9 bans it only in a test of the repository under test, where seeding behind the subject would prove nothing. An endpoint test's subject is the route, so the fastest honest way to put a row in front of it is to write one. The `make_foo` factory below seeds via a raw SQL `INSERT` through `session_factory` — that is the **relational-store** variant, valid when the resource is backed by a relational store. A resource backed by a client-style store (redis / a document store / …) has no `session_factory` and no SQL table: seed it either by **POSTing through the API** (drive the create endpoint, then test against the result) or via the **store's own client** in the fixture. Pick the path from the resource's datastore kind; don't reach for `INSERT INTO` when there is no SQL table.

```python
import uuid
from collections.abc import Awaitable, Callable

import pytest
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker


@pytest.fixture
def make_foo(session_factory: async_sessionmaker[AsyncSession]) -> Callable[..., Awaitable[uuid.UUID]]:
    async def _make(*, name: str | None = None) -> uuid.UUID:
        foo_id = uuid.uuid4()
        async with session_factory() as session:
            await session.execute(
                text("INSERT INTO foos(id, name) VALUES(:id, :name)"),
                {"id": str(foo_id), "name": name or f"foo-{foo_id}"},
            )
            await session.commit()
        return foo_id

    return _make
```

## Other bindings

- **Another way to drive the app.** An in-process ASGI transport is one; a framework's own test client,
  or a live server on a loopback port, are the others. What changes is how the client is constructed and
  whether the app's start-up events run — an out-of-process server runs them, an in-process transport may
  not, so a test that depends on start-up wiring has to arrange it. One file per endpoint, the by-value
  body assertion, the error-code assertion and per-test factory fixtures are unchanged.

## Rules

1. **One file per endpoint, named for its operation** (`test_create_foo.py`) — the subject is one route, not the router module. It holds the happy path and one test for each error code the route advertises (`hex-restapi-endpoint`); in an auth app the `401` and `403` are `hex-test-restapi-auth`'s. No mega-files spanning a whole resource.
2. **Assert the success body by value** — the fields the request set and those the operation promises to derive or leave alone. A framework that enforces the declared response model on the way out already turns a drifted body into a `500`; validate the whole body only where a route bypasses that model (a raw response).
3. **No cross-cutting registries but one.** Adding a new endpoint touches exactly one new test file. The one sanctioned table is the auth probe's public set: in an auth app, a route meant to be public is also named there (`hex-test-restapi-auth`). The discovered global checks — "every advertised error code carries the catalogue's shape", and in an auth app "every operation not declared public refuses an anonymous or untrusted caller" — are owned by `hex-test-app-invariants` and `hex-test-restapi-auth`, and derive their inputs from the app's published document and its resolved routes; there is no hand-maintained endpoint table to extend.
4. **Each test builds its own state** — `make_foo`, or a POST through the API; fixed natural keys and exact counts follow `test-principles`, *Assert strength*.
5. **An order assertion is reliable only where the seeded rows differ in the sort key**: a store that stamps rows inside one transaction can give them all one instant (`test-principles`, reliability 4), so sort by a key the test sets, or assert the set of ids. A mutation whose response carries no state (a `204` delete) is followed by a read through the API that proves the effect — the row's `404`, and a second row still served.
6. **The client is entered as a context manager, never assigned.** Bare-assigning it leaks the transport, and the leak surfaces as an unrelated test failing later in the session.
7. **Per-resource fixtures live in the sibling `conftest.py`.** Factory fixtures (`make_foo`) return one fresh row per call — a default for a unique column differs per call.
8. **Error responses are asserted by `code`, not by message.** `assert response.json()["code"] == ValidationError.code` — message text drifts, the `code` constant is the contract. The actual HTTP status is asserted separately.
9. **Mocking.** Follow `test-principles` for the mocking prohibition. If a test needs to mock, it isn't an integration test; move it to a domain unit test (`hex-test-domain`) or to a handler test over fakes (`hex-test-application-handler`), which is also where a compensating handler's undo is pinned.

## Inlined typing / import rules

- `pytest`, `httpx2`, `fastapi`, `myapp.domain.exceptions`. No `myapp.application.*` or `myapp.infrastructure.*` imports — the test drives over HTTP, not by reaching in.
- Full annotations on every signature, tests included — `-> None` on every test, every fixture parameter typed. `python-style` owns annotation policy and admits no test carve-out.
- No `from __future__ import annotations`.

## Hard stops

- Nothing up-tree builds the app on the test's own infrastructure bindings (`real_app` under this catalogue's binding) → stop, use `hex-test-integration-setup`; the missing thing is the guarantee, not the fixture name.
- Asked to register the new endpoint in a hand-maintained route or expectation table so a global check sees it → stop, use `hex-test-app-invariants`; the global checks derive their inputs from the running app, so a new route joins them with nothing to update. The auth probe's public set is the one exception (rule 3).
- A test asserts a role rejection or a cross-tenant 404 here → stop, use `hex-test-restapi-auth`; those assertions need a caller identity this skill does not mint.
- A test asserts on a response field that is not in the Pydantic response schema → stop, use `hex-restapi-schema` to extend the schema first.
- A test uses a plain `AsyncClient` for a request to a route that attaches an auth dependency → stop, use `hex-test-restapi-auth`'s authenticated client; a plain client on a gated route tests the rejection, not the endpoint.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.