skyzen 0.1.1

A fast, ergonomic HTTP framework that works everywhere
# Testing with Skyzen

Skyzen provides a dedicated testing crate (`skyzen-test`) with mock service implementations, an HTTP test client, response assertions, and snapshot testing — everything you need to test handlers without network I/O or external services.

## Setup

```toml
[dev-dependencies]
skyzen-test = { version = "0.1" }

# Enable a runtime feature for InMemoryDb (SQLite) support:
# skyzen-test = { version = "0.1", features = ["runtime-tokio-rustls"] }
```

## Mock Services

All mocks are in-memory, isolated per instance, and implement the same service traits as production backends.

### `InMemoryKv`

In-memory `KeyValueStore` implementation:

```rust
use skyzen_test::mock::InMemoryKv;
use skyzen_services::Kv;

let mock = InMemoryKv::new();
let kv = Kv::new(mock);

kv.put("key", b"value").await.unwrap();
let val = kv.get("key").await.unwrap();
assert_eq!(val, Some(b"value".to_vec()));
```

### `InMemoryStorage`

In-memory `ObjectStorage` implementation:

```rust
use skyzen_test::mock::InMemoryStorage;
use skyzen_services::Storage;

let mock = InMemoryStorage::new();
let storage = Storage::new(mock);

storage.put("file.txt", b"hello".to_vec()).await.unwrap();
let obj = storage.get("file.txt").await.unwrap().unwrap();
assert_eq!(obj.body, b"hello");
```

### `InMemoryQueue`

In-memory `MessageQueue` implementation:

```rust
use skyzen_test::mock::InMemoryQueue;
use skyzen_services::Queue;

let mock = InMemoryQueue::new();
let queue = Queue::new(mock);

queue.send(b"message").await.unwrap();
```

### `InMemoryDb`

SQLite in-memory database for SQL tests. Requires a runtime feature (`runtime-tokio-rustls` or `runtime-tokio-native-tls`).

```rust
use skyzen_test::mock::InMemoryDb;

let db = InMemoryDb::with_schema(
    "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL);",
).await.unwrap();

db.db()
    .query("INSERT INTO users (name) VALUES (?)")
    .bind("alice")
    .execute()
    .await
    .unwrap();
```

## TestClient

`TestClient` sends HTTP requests directly to an endpoint without network I/O. Create it via `TestContext`:

```rust
use skyzen_test::{TestContext, TestClient};

let ctx = TestContext::new();
let client = ctx.client(my_router);
```

### Building Requests

```rust
// GET request
let response = client.get("/users").send().await;

// POST with JSON body
let response = client
    .post("/users")
    .json(&serde_json::json!({"name": "alice"}))
    .send()
    .await;

// PUT with custom headers
let response = client
    .put("/users/1")
    .header("X-Custom", "value")
    .json(&update)
    .send()
    .await;

// DELETE with bearer auth
let response = client
    .delete("/users/1")
    .bearer("my-token")
    .send()
    .await;

// PATCH with raw body
let response = client
    .patch("/data")
    .body("raw bytes")
    .send()
    .await;
```

## Response Assertions

Every `send()` returns a `TestResponse` with rich assertion methods:

### Status Assertions

```rust
response.assert_status(200);           // Exact status code
response.assert_status_success();      // Any 2xx
response.assert_status_client_error(); // Any 4xx
response.assert_status_server_error(); // Any 5xx
```

### Header Assertions

```rust
response.assert_header("content-type", "application/json");
response.assert_header_exists("x-request-id");
```

### Body Assertions

```rust
response.assert_body_contains("hello");

// Deserialize JSON body
let user: User = response.assert_json();

// Assert a specific JSON path
response.assert_json_path("data.id", &serde_json::json!(42));
response.assert_json_path("users.0.name", &serde_json::json!("alice"));
```

### Accessors

```rust
let status = response.status();
let headers = response.headers();
let bytes = response.body_bytes();
let text = response.body_text();
let user: User = response.json();
```

## Snapshot Testing

The `SnapshotExt` trait adds snapshot assertions powered by [`insta`](https://insta.rs):

```rust
use skyzen_test::SnapshotExt;

let response = client.get("/api/users").send().await;
response.assert_snapshot("list_users_response");
```

JSON responses are automatically pretty-printed in snapshots. Manage snapshots with `cargo insta review`.

## Fixture Loading

Load test data from JSON strings:

```rust
use skyzen_test::fixtures::{from_json_str, from_json_array};

let user: User = from_json_str(r#"{"id": 1, "name": "alice"}"#).unwrap();
let users: Vec<User> = from_json_array(r#"[{"id": 1, "name": "alice"}]"#).unwrap();
```

## Full Example

Here's a complete integration test combining mocks, a test client, and assertions:

```rust
use skyzen::routing::{CreateRouteNode, Route, Router};
use skyzen::utils::Json;
use skyzen_services::Kv;
use skyzen_test::{TestContext, SnapshotExt};
use skyzen_test::mock::InMemoryKv;

// The handler under test — identical to production code
async fn get_greeting(kv: Kv) -> Result<String> {
    let name = kv.get_text("user:name").await?.unwrap_or("World".into());
    Ok(format!("Hello, {name}!"))
}

fn app(kv: Kv) -> Router {
    Route::new((
        "/greeting".at(get_greeting),
    ))
    .with(kv)
    .build()
}

#[tokio::test]
async fn test_greeting_with_stored_name() {
    // Set up mock
    let kv = Kv::new(InMemoryKv::new());
    kv.put("user:name", b"Skyzen").await.unwrap();

    // Build app with mock
    let ctx = TestContext::new();
    let client = ctx.client(app(kv));

    // Send request and assert
    let response = client.get("/greeting").send().await;
    response.assert_status(200);
    response.assert_body_contains("Hello, Skyzen!");
}

#[tokio::test]
async fn test_greeting_default() {
    let kv = Kv::new(InMemoryKv::new());

    let ctx = TestContext::new();
    let client = ctx.client(app(kv));

    let response = client.get("/greeting").send().await;
    response.assert_status(200);
    response.assert_body_contains("Hello, World!");
}
```

The key insight: **the handler code is production code**. Only the wiring in the test uses `InMemoryKv` instead of `Redis` or `DynamoKv`. This gives you confidence that the same handler will work correctly in production.