test(chapters): lock open-contribution upload contract + document intent
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -69,6 +69,21 @@ async fn get_one(
|
||||
Ok(Json(chapter))
|
||||
}
|
||||
|
||||
/// Add a chapter to a manga.
|
||||
///
|
||||
/// **Authorization is intentionally open**: any authenticated principal —
|
||||
/// a browser session *or* a bot API token — may add a chapter to *any*
|
||||
/// manga, including crawler-imported rows. This is by design: chapters are
|
||||
/// treated as community contributions, unlike the manga record itself
|
||||
/// (title/cover/metadata), whose edits gate through `require_can_edit`
|
||||
/// (see [`crate::api::mangas`]). The `CurrentUser` binding still requires a
|
||||
/// valid identity, so contributions are attributable, just not owner-scoped.
|
||||
///
|
||||
/// This contract is locked by `tests/api_chapters.rs`
|
||||
/// (`non_owner_can_upload_chapter`); changing it to owner-only is a
|
||||
/// deliberate decision, not a drive-by tightening. Until a richer
|
||||
/// contributor/moderation model lands this is acknowledged, intended
|
||||
/// behaviour — see the auth note in CLAUDE.md.
|
||||
async fn create(
|
||||
State(state): State<AppState>,
|
||||
CurrentUser(user): CurrentUser,
|
||||
|
||||
@@ -8,6 +8,8 @@ use uuid::Uuid;
|
||||
#[allow(unused_imports)]
|
||||
use serde_json as _;
|
||||
|
||||
use common::MultipartBuilder;
|
||||
|
||||
async fn seed_manga(h: &common::Harness, cookie: &str, title: &str) -> Uuid {
|
||||
common::seed_manga_via_api(&h.app, cookie, title).await
|
||||
}
|
||||
@@ -272,3 +274,44 @@ async fn list_pages_returns_404_for_unknown_chapter(pool: PgPool) {
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[sqlx::test(migrations = "./migrations")]
|
||||
async fn non_owner_can_upload_chapter(pool: PgPool) {
|
||||
// Contract lock: chapter upload is INTENTIONALLY open to any
|
||||
// authenticated user, not just the manga's creator. A user who did not
|
||||
// create the manga can still contribute a chapter (community
|
||||
// contributions), unlike manga-record edits which gate on ownership.
|
||||
// If this ever needs to become owner-only, that's a deliberate change —
|
||||
// this test (and the doc comment on `api::chapters::create`) should be
|
||||
// updated together, not silently broken.
|
||||
let h = common::harness(pool);
|
||||
|
||||
// Owner creates the manga.
|
||||
let (_owner, owner_cookie) = common::register_user(&h.app).await;
|
||||
let manga_id = seed_manga(&h, &owner_cookie, "Berserk").await;
|
||||
|
||||
// A different, non-owner user uploads a chapter to it.
|
||||
let (_other, other_cookie) = common::register_user(&h.app).await;
|
||||
let resp = h
|
||||
.app
|
||||
.clone()
|
||||
.oneshot(common::post_multipart_with_cookie(
|
||||
&format!("/api/v1/mangas/{manga_id}/chapters"),
|
||||
MultipartBuilder::new()
|
||||
.add_json("metadata", json!({ "number": 1, "title": "Contributed" }))
|
||||
.add_file("page", "1.png", "image/png", &common::fake_png_bytes()),
|
||||
&other_cookie,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
resp.status(),
|
||||
StatusCode::CREATED,
|
||||
"a non-owner authenticated user must be able to upload a chapter"
|
||||
);
|
||||
let body = common::body_json(resp).await;
|
||||
assert_eq!(body["number"], 1);
|
||||
assert_eq!(body["title"], "Contributed");
|
||||
assert_eq!(body["page_count"], 1);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user