mod common; use axum::http::StatusCode; use serde_json::json; use sqlx::PgPool; use tower::ServiceExt; #[sqlx::test(migrations = "./migrations")] async fn list_is_empty_initially(pool: PgPool) { let h = common::harness(pool); let resp = h.app.oneshot(common::get("/api/v1/mangas")).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); let body = common::body_json(resp).await; assert_eq!(body["items"], json!([])); assert_eq!(body["page"]["limit"], 50); assert_eq!(body["page"]["offset"], 0); assert!(body["page"]["total"].is_null()); } #[sqlx::test(migrations = "./migrations")] async fn create_then_list_roundtrip(pool: PgPool) { let h = common::harness(pool); let (_, cookie) = common::register_user(&h.app).await; let created = h .app .clone() .oneshot(common::post_json_with_cookie( "/api/v1/mangas", json!({ "title": "Berserk", "author": "Kentaro Miura", "description": null }), &cookie, )) .await .unwrap(); assert_eq!(created.status(), StatusCode::OK); let body = common::body_json(created).await; assert_eq!(body["title"], "Berserk"); assert_eq!(body["author"], "Kentaro Miura"); assert!(body["id"].as_str().is_some()); let listed = h.app.oneshot(common::get("/api/v1/mangas")).await.unwrap(); let listed_body = common::body_json(listed).await; let items = listed_body["items"].as_array().unwrap(); assert_eq!(items.len(), 1); assert_eq!(items[0]["title"], "Berserk"); } #[sqlx::test(migrations = "./migrations")] async fn search_filters_by_title_and_author(pool: PgPool) { let h = common::harness(pool); let (_, cookie) = common::register_user(&h.app).await; for (title, author) in [ ("One Piece", "Eiichiro Oda"), ("Berserk", "Kentaro Miura"), ("Vinland Saga", "Makoto Yukimura"), ] { let _ = h .app .clone() .oneshot(common::post_json_with_cookie( "/api/v1/mangas", json!({ "title": title, "author": author }), &cookie, )) .await .unwrap(); } let resp = h .app .clone() .oneshot(common::get("/api/v1/mangas?search=miura")) .await .unwrap(); let body = common::body_json(resp).await; let titles: Vec<&str> = body["items"] .as_array() .unwrap() .iter() .map(|m| m["title"].as_str().unwrap()) .collect(); assert_eq!(titles, vec!["Berserk"]); let resp = h .app .oneshot(common::get("/api/v1/mangas?search=saga")) .await .unwrap(); let body = common::body_json(resp).await; let titles: Vec<&str> = body["items"] .as_array() .unwrap() .iter() .map(|m| m["title"].as_str().unwrap()) .collect(); assert_eq!(titles, vec!["Vinland Saga"]); } #[sqlx::test(migrations = "./migrations")] async fn create_rejects_empty_title_with_envelope(pool: PgPool) { let h = common::harness(pool); let (_, cookie) = common::register_user(&h.app).await; let resp = h .app .oneshot(common::post_json_with_cookie( "/api/v1/mangas", json!({ "title": " ", "author": null }), &cookie, )) .await .unwrap(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); let body = common::body_json(resp).await; assert_eq!(body["error"]["code"], "invalid_input"); let msg = body["error"]["message"].as_str().expect("message is string"); assert!(!msg.is_empty(), "message should be non-empty"); } #[sqlx::test(migrations = "./migrations")] async fn create_requires_authentication(pool: PgPool) { let h = common::harness(pool); let resp = h .app .oneshot(common::post_json( "/api/v1/mangas", json!({ "title": "Berserk" }), )) .await .unwrap(); assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); let body = common::body_json(resp).await; assert_eq!(body["error"]["code"], "unauthenticated"); } #[sqlx::test(migrations = "./migrations")] async fn get_unknown_id_is_404_with_envelope(pool: PgPool) { let h = common::harness(pool); let resp = h .app .oneshot(common::get( "/api/v1/mangas/00000000-0000-0000-0000-000000000000", )) .await .unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); let body = common::body_json(resp).await; assert_eq!(body["error"]["code"], "not_found"); let msg = body["error"]["message"].as_str().expect("message is string"); assert!(!msg.is_empty(), "message should be non-empty"); }