--- # quotesdb-gu9j title: Implement test server harness — spawn quotesdb-api with temp SQLite DB, return base URL status: completed type: task priority: critical created_at: 2026-03-10T23:32:09Z updated_at: 2026-03-10T23:32:16Z blocked_by: - quotesdb-bl2g - quotesdb-aa9s - quotesdb-jpu5 --- Integration tests live in `tests/` and exercise the API binary against a temporary SQLite database. They run with `cargo test` and must not require a running Cloudflare environment. Architecture decided in triage: - 2ab7a8: Server is spawned as a tokio task using the native Axum path (cfg-split, no workers-rs on host) - fba598: Isolation strategy is **per-test temp SQLite file** via `tempfile` crate (transaction rollback cannot intercept server-side pool commits; in-memory SQLite is incompatible with multi-connection SQLx pools) - 0d84fa: HTTP client for tests is `reqwest` with `tokio::test` Implement `tests/helpers.rs` providing a `spawn_test_server()` async function that: 1. Creates a temporary SQLite file via `tempfile::TempDir` 2. Opens a `SqlitePool` connected to that file 3. Runs migrations via `sqlx::migrate!()` 4. Builds the Axum router via `build_router(repo)` (same router used by the API binary) 5. Binds to a random port with `TcpListener::bind("127.0.0.1:0")` 6. Spawns the server with `tokio::spawn(axum::serve(...))` 7. Returns a `TestContext` that holds the `TempDir` (RAII cleanup), base URL, and task handle ```rust // tests/helpers.rs use std::sync::Arc; use tempfile::TempDir; use tokio::net::TcpListener; use sqlx::SqlitePool; pub struct TestContext { _db_dir: TempDir, // deleted on drop pub base_url: String, _server: tokio::task::JoinHandle<()>, } pub async fn spawn_test_server() -> TestContext { let db_dir = TempDir::new().expect("temp dir"); let db_path = db_dir.path().join("test.sqlite"); let db_url = format!("sqlite:{}?mode=rwc", db_path.display()); let pool = SqlitePool::connect(&db_url).await.expect("pool"); sqlx::migrate!("./migrations").run(&pool).await.expect("migrations"); let repo = Arc::new(NativeRepository::new(pool)); let app = build_router(repo); let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind"); let port = listener.local_addr().unwrap().port(); let server = tokio::spawn(async move { axum::serve(listener, app).await.unwrap(); }); TestContext { _db_dir: db_dir, base_url: format!("http://127.0.0.1:{port}"), _server: server, } } ``` Usage in a test: ```rust #[tokio::test] async fn test_create_quote() { let ctx = spawn_test_server().await; let client = reqwest::Client::new(); let res = client .put(format!("{}/api/quotes", ctx.base_url)) .json(&serde_json::json!({"text": "hello", "author": "world"})) .send() .await .unwrap(); assert_eq!(res.status(), 201); } ``` - `build_router` and `NativeRepository` must be pub-accessible from the `quotesdb` crate (may require re-exports in `src/lib.rs`). - `sqlx::migrate!()` macro path is relative to the crate root — migrations must be in `migrations/` at the crate root. - Each test gets a unique `TempDir`, so parallel test execution (`cargo test`) is safe. - Do not set `--test-threads=1`; parallel execution must work. - The `_server` handle is intentionally leaked (tokio runtime drops it when the test ends). In `[dev-dependencies]` (ticket 5f5ba0): - `tempfile = "3"` - `reqwest = { version = "0.12", features = ["json"] }` - `tokio = { version = "1", features = ["full"] }` - `serde_json = "1"` Use `superpowers:test-driven-development` — the harness is itself tested by running `cargo test`. Use `superpowers:verification-before-completion` before closing. Run in order from the `quotesdb/` directory: ```sh cargo fmt cargo check cargo clippy cargo test ``` `test(quotesdb): implement test server harness with per-test temp SQLite DB`