veer 0.3.0

Inertia.js v3 server-side protocol superset for Rust
Documentation
# Testing

The `testing` feature has helpers for tests of your handlers. Enable it for tests only:

```toml
[dev-dependencies]
veer = { version = "0.3", features = ["testing"] }
tower = { version = "0.5", features = ["util"] }
```

## A page test

Build your app as in `main`, send a request with `tower::ServiceExt::oneshot`, and read the page:

```rust,ignore
use tower::ServiceExt;
use veer::testing::{visit, TestPage};

#[tokio::test]
async fn users_index_lists_the_users() {
    let response = app().oneshot(visit("GET", "/users")).await.unwrap();

    let page = TestPage::from_response(response).await;
    assert_eq!(page.component, "Users/Index");
    assert_eq!(page.prop("users.0.name"), Some(&json!("Ada")));
    assert_eq!(page.prop("auth.user"), Some(&json!(null)));
}
```

- **`visit(method, uri)`** makes an Inertia request (`X-Inertia: true`) with an empty body. It sends the asset version `"1"`, which is the default. If your test config has a different version, replace the `X-Inertia-Version` header.
- **`TestPage::from_response`** reads the page object from a JSON response or from the HTML of a first page load. It panics with the status and the body if the response is not a page.
- **`page.prop(path)`** reads a prop at a dot path; a number is an array index. `page.props` and `page.raw` are the complete JSON values.

## Forms, errors and flash data

`MemorySession` is a session store in memory. Its clones share the data, so that you can follow a redirect or look at the data that the next request will get:

```rust,ignore
use veer::testing::MemorySession;

#[tokio::test]
async fn store_rejects_a_short_title() {
    let session = MemorySession::default();
    let app = app(InertiaConfig::new().session(session.clone()));

    let request = Request::post("/todos")
        .header("x-inertia", "true")
        .header("content-type", "application/json")
        .header("referer", "/todos/new")
        .body(Body::from(json!({ "title": "a" }).to_string()))
        .unwrap();
    let response = app.clone().oneshot(request).await.unwrap();

    assert_eq!(response.status(), StatusCode::SEE_OTHER);
    assert_eq!(response.headers()["location"], "/todos/new");
    assert!(session.flash().errors.contains_key("title"));

    // The page after the redirect has the error.
    let page = TestPage::from_response(app.oneshot(visit("GET", "/todos/new")).await.unwrap()).await;
    assert!(page.prop("errors.title").is_some());
}
```