Move every handler from /api/* to /api/v1/*. /api/* is now reserved for
future versioning.
Standardise the error response shape across the API as
{"error": {"code": "snake_case", "message": "..."}}. AppError gains a
`code()` whose top-level variants are matched exhaustively without a
wildcard — new variants are a compile error until coded. 500-class
responses always emit the fixed "internal error" string and log the
real cause via tracing only.
Lock in the list pagination envelope as {"items": [...], "page": {
"limit", "offset", "total"}} and apply it to GET /api/v1/mangas. `total`
serialises as null until feat/list-search-polish lands an indexed count.
The frontend client parses the envelope into ApiError.code with an
http_error fallback for non-JSON bodies. listMangas now returns the
paged shape; the root route consumes .items. New client.test.ts covers
envelope parsing and the fallback paths.
Lockstep version bump to 0.2.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
50 lines
1.4 KiB
Rust
50 lines
1.4 KiB
Rust
// Shared test helpers. Each integration test binary picks the subset it needs,
|
|
// so dead-code lints on the unused helpers fire per-binary; suppress at the
|
|
// module level.
|
|
#![allow(dead_code)]
|
|
|
|
use std::sync::Arc;
|
|
|
|
use axum::body::Body;
|
|
use axum::http::Request;
|
|
use axum::Router;
|
|
use http_body_util::BodyExt;
|
|
use sqlx::PgPool;
|
|
use tempfile::TempDir;
|
|
|
|
use mangalord::app::{router, AppState};
|
|
use mangalord::storage::LocalStorage;
|
|
|
|
pub struct Harness {
|
|
pub app: Router,
|
|
// Kept alive for the lifetime of the test so the temp dir is not dropped.
|
|
pub _storage_dir: TempDir,
|
|
}
|
|
|
|
pub fn harness(pool: PgPool) -> Harness {
|
|
let storage_dir = tempfile::tempdir().expect("tempdir");
|
|
let state = AppState {
|
|
db: pool,
|
|
storage: Arc::new(LocalStorage::new(storage_dir.path())),
|
|
};
|
|
Harness { app: router(state), _storage_dir: storage_dir }
|
|
}
|
|
|
|
pub async fn body_json(response: axum::response::Response) -> serde_json::Value {
|
|
let bytes = response.into_body().collect().await.unwrap().to_bytes();
|
|
serde_json::from_slice(&bytes).expect("body is JSON")
|
|
}
|
|
|
|
pub fn get(uri: &str) -> Request<Body> {
|
|
Request::builder().uri(uri).body(Body::empty()).unwrap()
|
|
}
|
|
|
|
pub fn post_json(uri: &str, body: serde_json::Value) -> Request<Body> {
|
|
Request::builder()
|
|
.method("POST")
|
|
.uri(uri)
|
|
.header("content-type", "application/json")
|
|
.body(Body::from(body.to_string()))
|
|
.unwrap()
|
|
}
|