test(chapters): lock open-contribution upload contract + document intent

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-01 07:19:04 +02:00
parent 2af421893f
commit a3e53f303b
2 changed files with 58 additions and 0 deletions

View File

@@ -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,

View File

@@ -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);
}