atrg-testing 0.1.2

Test utilities for at-rust-go applications
Documentation

atrg-testing

Test utilities for at-rust-go: mock clients, fake Jetstream, and in-memory test harness.

Part of at-rust-go (atrg) — a batteries-included AT Protocol backend framework for Rust.

What this crate provides

  • test_state() — builds an AppState backed by in-memory SQLite with migrations pre-applied and no outbound network calls.
  • test_app() — returns a ready-to-use (AtrgApp, AppState) pair for tower::ServiceExt::oneshot testing.
  • seed_session() — inserts a fake authenticated session into the test database so handlers that require AuthUser / RequireAuth work without an OAuth round-trip.
  • MockAtprotoClient — programmable stub for AT Protocol XRPC calls (get_record, put_record, etc.). Records every call for assertions.
  • FakeJetstream — feeds deterministic JetstreamEvents into your on_event handler without touching the network.

Usage

Add atrg-testing as a dev dependency:

[dev-dependencies]
atrg-testing = { version = "0.1" }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
tower = { version = "0.5", features = ["util"] }
http-body-util = "0.1"

Testing a JSON handler

use axum::{routing::get, Json, Router};
use atrg_core::AppState;
use atrg_testing::test_state;
use tower::ServiceExt;

async fn hello() -> Json<serde_json::Value> {
    Json(serde_json::json!({ "status": "ok" }))
}

#[tokio::test]
async fn test_hello_handler() {
    let state = test_state().await;

    let app = Router::new()
        .route("/", get(hello))
        .with_state(state);

    let response = app
        .oneshot(
            axum::http::Request::builder()
                .uri("/")
                .body(axum::body::Body::empty())
                .unwrap(),
        )
        .await
        .unwrap();

    assert_eq!(response.status(), 200);
}

Testing an authenticated handler

use atrg_testing::{test_state, seed_session};

#[tokio::test]
async fn test_authenticated_route() {
    let state = test_state().await;

    // Seed a session so RequireAuth succeeds
    let session_id = seed_session(
        &state.db,
        "did:plc:testuser",
        "test.handle",
    )
    .await;

    // Use session_id as a Bearer token or cookie in your request
}

Using FakeJetstream

use atrg_testing::FakeJetstream;

#[tokio::test]
async fn test_event_handler() {
    let state = test_state().await;
    let mut fake = FakeJetstream::new();

    fake.push_commit("did:plc:abc", "app.bsky.feed.post", "rkey123", serde_json::json!({
        "text": "hello from test",
        "createdAt": "2025-01-01T00:00:00Z",
    }));

    // Feed events into your handler
    for event in fake.drain() {
        my_app::handle_event(event, state.clone()).await.unwrap();
    }
}

Design philosophy

  • Zero network access. Every component is backed by in-memory stores and stubs.
  • Deterministic. No randomness, no real clocks unless you opt in.
  • Fast. In-memory SQLite + oneshot means sub-millisecond test execution.

Related crates

Crate Role
atrg-core AppState and app builder under test
atrg-auth Session types used by seed_session
atrg-db In-memory SQLite pool and migrations
atrg-stream Real Jetstream consumer (replaced by FakeJetstream in tests)

License

LGPL-3.0-only — see LICENSE.