mod common; use axum::http::StatusCode; use serde_json::json; use sqlx::PgPool; use tower::ServiceExt; use common::MultipartBuilder; fn metadata(title: &str) -> serde_json::Value { json!({ "title": title }) } #[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_multipart_with_cookie( "/api/v1/mangas", MultipartBuilder::new().add_json( "metadata", json!({ "title": "Berserk", "author": "Kentaro Miura", "description": null }), ), &cookie, )) .await .unwrap(); assert_eq!(created.status(), StatusCode::CREATED); 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_multipart_with_cookie( "/api/v1/mangas", MultipartBuilder::new() .add_json("metadata", 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_validation_failed(pool: PgPool) { let h = common::harness(pool); let (_, cookie) = common::register_user(&h.app).await; let resp = h .app .oneshot(common::post_multipart_with_cookie( "/api/v1/mangas", MultipartBuilder::new().add_json("metadata", metadata(" ")), &cookie, )) .await .unwrap(); assert_eq!(resp.status(), StatusCode::UNPROCESSABLE_ENTITY); let body = common::body_json(resp).await; assert_eq!(body["error"]["code"], "validation_failed"); assert!(body["error"]["details"]["title"].is_string()); } #[sqlx::test(migrations = "./migrations")] async fn create_rejects_missing_metadata_part(pool: PgPool) { let h = common::harness(pool); let (_, cookie) = common::register_user(&h.app).await; let resp = h .app .oneshot(common::post_multipart_with_cookie( "/api/v1/mangas", MultipartBuilder::new(), // no metadata part &cookie, )) .await .unwrap(); assert_eq!(resp.status(), StatusCode::UNPROCESSABLE_ENTITY); let body = common::body_json(resp).await; assert_eq!(body["error"]["code"], "validation_failed"); assert_eq!(body["error"]["details"]["metadata"], "required"); } #[sqlx::test(migrations = "./migrations")] async fn create_requires_authentication(pool: PgPool) { let h = common::harness(pool); let resp = h .app .oneshot(common::post_multipart( "/api/v1/mangas", MultipartBuilder::new().add_json("metadata", metadata("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"); }