mod common; use axum::http::StatusCode; use serde_json::json; use sqlx::PgPool; use tower::ServiceExt; use uuid::Uuid; #[sqlx::test(migrations = "./migrations")] async fn create_then_list_returns_only_own(pool: PgPool) { let h = common::harness(pool); let (_, cookie_a) = common::register_user(&h.app).await; let (_, cookie_b) = common::register_user(&h.app).await; let manga_id = common::seed_manga_via_api(&h.app, &cookie_a, "Berserk").await; // User A bookmarks the manga. let resp = h .app .clone() .oneshot(common::post_json_with_cookie( "/api/v1/bookmarks", json!({ "manga_id": manga_id.to_string() }), &cookie_a, )) .await .unwrap(); assert_eq!(resp.status(), StatusCode::CREATED); let body = common::body_json(resp).await; assert_eq!(body["manga_id"], manga_id.to_string()); // User B sees nothing. let resp = h .app .clone() .oneshot(common::get_with_cookie("/api/v1/me/bookmarks", &cookie_b)) .await .unwrap(); assert_eq!(resp.status(), StatusCode::OK); let body = common::body_json(resp).await; assert_eq!(body["items"], json!([])); // User A sees their bookmark. let resp = h .app .oneshot(common::get_with_cookie("/api/v1/me/bookmarks", &cookie_a)) .await .unwrap(); let body = common::body_json(resp).await; let items = body["items"].as_array().unwrap(); assert_eq!(items.len(), 1); assert_eq!(items[0]["manga_id"], manga_id.to_string()); } #[sqlx::test(migrations = "./migrations")] async fn create_returns_409_on_duplicate_manga_level(pool: PgPool) { let h = common::harness(pool); let (_, cookie) = common::register_user(&h.app).await; let manga_id = common::seed_manga_via_api(&h.app, &cookie, "Berserk").await; let make = || { common::post_json_with_cookie( "/api/v1/bookmarks", json!({ "manga_id": manga_id.to_string() }), &cookie, ) }; let first = h.app.clone().oneshot(make()).await.unwrap(); assert_eq!(first.status(), StatusCode::CREATED); let second = h.app.oneshot(make()).await.unwrap(); assert_eq!(second.status(), StatusCode::CONFLICT); let body = common::body_json(second).await; assert_eq!(body["error"]["code"], "conflict"); } #[sqlx::test(migrations = "./migrations")] async fn create_404_on_unknown_manga(pool: PgPool) { let h = common::harness(pool); let (_, cookie) = common::register_user(&h.app).await; let unknown = Uuid::nil(); let resp = h .app .oneshot(common::post_json_with_cookie( "/api/v1/bookmarks", json!({ "manga_id": unknown.to_string() }), &cookie, )) .await .unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); } #[sqlx::test(migrations = "./migrations")] async fn create_requires_authentication(pool: PgPool) { let h = common::harness(pool); let (_, cookie) = common::register_user(&h.app).await; let manga_id = common::seed_manga_via_api(&h.app, &cookie, "Berserk").await; // Unauthenticated request → 401. let resp = h .app .oneshot(common::post_json( "/api/v1/bookmarks", json!({ "manga_id": manga_id.to_string() }), )) .await .unwrap(); assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); } #[sqlx::test(migrations = "./migrations")] async fn user_a_cannot_delete_user_b_bookmark(pool: PgPool) { let h = common::harness(pool); let (_, cookie_a) = common::register_user(&h.app).await; let (_, cookie_b) = common::register_user(&h.app).await; let manga_id = common::seed_manga_via_api(&h.app, &cookie_a, "Berserk").await; // User A creates a bookmark. let resp = h .app .clone() .oneshot(common::post_json_with_cookie( "/api/v1/bookmarks", json!({ "manga_id": manga_id.to_string() }), &cookie_a, )) .await .unwrap(); let body = common::body_json(resp).await; let id = body["id"].as_str().unwrap().to_string(); // User B tries to delete → 403. let resp = h .app .clone() .oneshot(common::delete_with_cookie( &format!("/api/v1/bookmarks/{id}"), &cookie_b, )) .await .unwrap(); assert_eq!(resp.status(), StatusCode::FORBIDDEN); let body = common::body_json(resp).await; assert_eq!(body["error"]["code"], "forbidden"); // User A succeeds. let resp = h .app .oneshot(common::delete_with_cookie( &format!("/api/v1/bookmarks/{id}"), &cookie_a, )) .await .unwrap(); assert_eq!(resp.status(), StatusCode::NO_CONTENT); } #[sqlx::test(migrations = "./migrations")] async fn delete_unknown_bookmark_is_404(pool: PgPool) { let h = common::harness(pool); let (_, cookie) = common::register_user(&h.app).await; let resp = h .app .oneshot(common::delete_with_cookie( "/api/v1/bookmarks/00000000-0000-0000-0000-000000000000", &cookie, )) .await .unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); } #[sqlx::test(migrations = "./migrations")] async fn delete_already_deleted_bookmark_is_404(pool: PgPool) { let h = common::harness(pool); let (_, cookie) = common::register_user(&h.app).await; let manga_id = common::seed_manga_via_api(&h.app, &cookie, "Berserk").await; let resp = h .app .clone() .oneshot(common::post_json_with_cookie( "/api/v1/bookmarks", json!({ "manga_id": manga_id.to_string() }), &cookie, )) .await .unwrap(); let id = common::body_json(resp).await["id"].as_str().unwrap().to_string(); let resp = h .app .clone() .oneshot(common::delete_with_cookie( &format!("/api/v1/bookmarks/{id}"), &cookie, )) .await .unwrap(); assert_eq!(resp.status(), StatusCode::NO_CONTENT); // Deleting again → 404, not 500. let resp = h .app .oneshot(common::delete_with_cookie( &format!("/api/v1/bookmarks/{id}"), &cookie, )) .await .unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); } #[sqlx::test(migrations = "./migrations")] async fn list_me_requires_authentication(pool: PgPool) { let h = common::harness(pool); let resp = h .app .oneshot(common::get("/api/v1/me/bookmarks")) .await .unwrap(); assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); } #[sqlx::test(migrations = "./migrations")] async fn list_me_returns_paged_envelope(pool: PgPool) { let h = common::harness(pool); let (_, cookie) = common::register_user(&h.app).await; let resp = h .app .oneshot(common::get_with_cookie("/api/v1/me/bookmarks", &cookie)) .await .unwrap(); assert_eq!(resp.status(), StatusCode::OK); let body = common::body_json(resp).await; assert!(body["items"].is_array()); assert_eq!(body["page"]["limit"], 50); assert_eq!(body["page"]["offset"], 0); assert!(body["page"]["total"].is_null()); }