# Write Integration Tests for an Axum API

Yes. An agent writes tests under tests/ that call every Axum route through tower's oneshot against a real Postgres, runs cargo test and opens a PR.

[Run this on Rush](https://cloud.getrush.ai/run/write-rust-integration-tests/axum/), in a Project that has your repository.

## What the agent does

The agent starts with `Cargo.toml` and the router, because how an Axum app is assembled decides how a test can reach it. In an API like launchbadge/realworld-axum-sqlx it finds `axum` 0.3.4, `sqlx` 0.5 with the `postgres`, `uuid` and `time` features, and `tower` 0.4. `src/http/mod.rs` builds the app in a private `api_router()` that merges `users::router()`, `profiles::router()` and `articles::router()`, and the only public entry is `serve(config, db)`, which binds `0.0.0.0:8080`. A test in `tests/` compiles as a separate crate and cannot call a private function, so the one source change the agent makes is a small public constructor in `src/http/mod.rs` that returns the `Router` with its `ApiContext` extension layer, which `serve` then calls. Behaviour does not change; the pull request says so.

Next it sets up the database the way the code expects. The SQLx `query!` and `query_as!` macros check every query against a live database at compile time, and the repository has no offline query data, so `cargo test` will not even build without a migrated Postgres behind `DATABASE_URL`. The agent starts Postgres in its sandbox, runs `sqlx db setup` the way `.github/workflows/rust.yml` does, and sets `HMAC_KEY`, the second field `src/config.rs` reads. The `#[sqlx::test]` attribute arrived in SQLx 0.6, so on 0.5 it writes a helper in `tests/common/mod.rs` that creates a uniquely named database per test, applies `sqlx::migrate!()` to it, and builds the app on that pool, so tests run in parallel without sharing rows.

Then it covers the routes the routers declare, one file per area: `tests/users.rs` for `POST /api/users`, `POST /api/users/login` and `GET` and `PUT /api/user`; `tests/profiles.rs` for `/api/profiles/:username` and its `follow`; `tests/articles.rs` for create, list, the feed, read, update, delete, favorite and `GET /api/tags`; and `tests/comments.rs` for `/api/articles/:slug/comments` and deleting one by id. Each test builds a `Request`, sends it with `tower::ServiceExt::oneshot`, and asserts on the status and the JSON body. It checks the failure paths `src/http/error.rs` defines: `401` with a `WWW-Authenticate: Token` header when the `Authorization: Token ...` header is missing or forged, `403` when one user edits another's article, `404` for an unknown slug, and `422` with an `errors` map when a username or email hits the `user_username_key` or `user_email_key` constraint.

It runs `cargo test` and reads the output, including the existing `test_slugify` unit test in `src/http/articles/mod.rs`, which must still pass. A test that fails because of the code rather than the test stays in the pull request marked `#[ignore]` with a note explaining what the route returned and what the RealWorld spec asks for, so the suite is green and the finding is not lost.

## Start it with one API call

`POST https://api.prix.dev/o/{org}/sessions`

```json
{
  "agent": "claude",
  "input": "Write integration tests under tests/ that start this Axum app against a real Postgres with SQLx migrations and call every route through tower::ServiceExt::oneshot. Start Postgres in the sandbox and set DATABASE_URL so the sqlx query macros compile. If the router is not reachable from an integration test, add the smallest public constructor that returns it and have the existing serve function use it; change nothing else in src/. Give each test its own database with migrations applied, so tests can run in parallel. For every route assert the success status and JSON body, and cover missing or invalid auth tokens, editing another user's resource, unknown ids and duplicate usernames or emails. Run cargo test, and if a test fails because of the application rather than the test, mark it #[ignore] with a comment saying what the route returned. Open a pull request listing each route and the cases tested.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-api"
    }
  ]
}
```

1. **Get an API key.** In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
2. **Send the request.** POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
3. **Read what came back.** Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.

## What comes back

The pull request adds a `tests/` directory with a shared `common` module and one file per router, plus the small change in `src/http/mod.rs` that makes the router constructible from a test. For a codebase shaped like realworld-axum-sqlx, `Cargo.toml` gains dev dependencies only, typically `serde_json` for reading bodies and `tower` features the tests need, and `Cargo.lock` changes to match. The description lists every route from the routers with the cases each test covers, and names any route left out with the reason.

Its proof is the run in two places. The repository's existing Rust workflow, which already starts a `postgres` service and runs `cargo test --verbose`, picks the new tests up on the pull request with no workflow change. And the session record keeps the agent's own `cargo test` output from its sandbox, so a reviewer can see each test name pass, and read any `#[ignore]` case with the response that caused it.

Before merging, a reviewer checks that the new public constructor exposes nothing beyond the router, that the per-test database helper drops or reuses databases so a long-running CI host does not fill up, and that assertions check the RealWorld response shapes the frontend relies on, such as the `user.token` field after login, rather than only status codes.

## Run it on every route added without a test

Run it from a GitHub webhook on `pull_request`: when a pull request changes a file under `src/http/` and no file under `tests/`, your server sends this request with the pull request URL in `resources` (`https://github.com/<owner>/<repo>/pull/<n>`), so the agent works on that branch and adds tests for the routes it touched. Each pull request gets its own session in its own sandbox with its own Postgres.

A webhook URL set on the API key receives `run.completed` or `run.failed` when the turn ends, with the session URL, so your server can comment on the original pull request with what was added.

## Why run it on Rush?

An SQLx project with checked queries does not compile without a migrated database, so writing its first integration tests on a laptop means installing Postgres, running `sqlx-cli`, setting `DATABASE_URL` and keeping a database you then have to reset. On Rush the agent starts Postgres in its own sandbox, applies the migrations, compiles and runs the suite there, and nothing is installed on your machine or left running afterwards.

Every `cargo build` and `cargo test` the agent ran stays in the session with its full output, so a claim in the pull request such as "`PUT /api/articles/:slug` returns 403 for another user" can be checked against the test that proves it. The same request can run on several Rust services at once, each in its own sandbox, each with a pull request someone reviews.

## Frequently asked questions

### Why tower's oneshot instead of starting the server on a port?

`oneshot` sends one request straight into the `Router` as a tower `Service`, with no socket, no port to pick and no wait for the server to start. Tests stay fast and run in parallel. The request and response are the same types a real server handles, so routing, extractors and error conversion are all exercised.

### Does it work with #[sqlx::test]?

Yes, on SQLx 0.6 and later. That attribute creates a fresh test database per test, applies the migrations and hands the test a pool, so the agent uses it instead of a hand-written helper. On SQLx 0.5, as in realworld-axum-sqlx, the attribute does not exist, and the agent writes the equivalent helper in `tests/common/mod.rs`.

### Will it change my application code?

Only if the router cannot be built from outside the crate, and then by the smallest public constructor that returns it, used by the existing server function. The prompt forbids any other change under `src/`, and the diff shows exactly what moved. A bug a test finds is reported as an ignored test, not fixed.

### What about the sqlx offline query data?

The agent compiles against a live database in the sandbox, so it does not need it. If your CI builds with `SQLX_OFFLINE=true`, ask in the prompt and it runs `cargo sqlx prepare` and commits the generated query data, so the build no longer needs a database to compile.
