test: tighten audit-flagged tests and add missing coverage
Tightens three tests whose names overstated what they checked: - `login_succeeds_and_rotates_session` now asserts the login cookie differs from the registration cookie, and that the registration cookie is still valid after login (the documented contract). - `storage::local::rejects_path_traversal` exercises three extra rejection paths the existing implementation already handled but the tests didn't probe: `a/./b`, the single-segment `.`, and the empty segment `a//b`. - `create_and_use_bot_token` asserts that `token_hash` is *absent* from the response (`get(...).is_none()`), not just `is_null()`, which would have accepted an explicit `"token_hash": null` payload too. Adds four coverage cases that the audit flagged as missing: - `me_rejects_expired_session` — hand-craft a session row with `expires_at = now() - 1h`, hit `/auth/me` with the matching cookie, expect 401 + `unauthenticated`. Proves the extractor's `expires_at > now()` filter is wired. - `concurrent_manga_bookmarks_serialised_by_unique_index` — spawn two POSTs in parallel for the same `(user, manga, chapter=null)`, assert one wins (201) and one collides (409) via the partial unique index from migration 0004. - `bookmark_create_accepts_bearer_token` — mint a bot token and POST /bookmarks with `Authorization: Bearer`, asserting `CurrentUser` resolves identically to the cookie path on a write endpoint (not just `/auth/me`). - Three new unit tests on `app::cors_layer` covering the allowlist (origin reflected, credentials true), a foreign origin (no allow-origin header emitted), and the same-origin default (empty allowlist emits no CORS headers at all). `cors_layer` is `pub(crate)` now so the tests in `app::tests` can reach it; the function itself is unchanged. No version bump. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -187,6 +187,90 @@ async fn user_a_cannot_delete_user_b_bookmark(pool: PgPool) {
|
||||
assert_eq!(resp.status(), StatusCode::NO_CONTENT);
|
||||
}
|
||||
|
||||
#[sqlx::test(migrations = "./migrations")]
|
||||
async fn concurrent_manga_bookmarks_serialised_by_unique_index(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 app_a = h.app.clone();
|
||||
let cookie_a = cookie.clone();
|
||||
let mid_a = manga_id.to_string();
|
||||
let app_b = h.app.clone();
|
||||
let cookie_b = cookie.clone();
|
||||
let mid_b = manga_id.to_string();
|
||||
|
||||
let f1 = tokio::spawn(async move {
|
||||
app_a
|
||||
.oneshot(common::post_json_with_cookie(
|
||||
"/api/v1/bookmarks",
|
||||
json!({ "manga_id": mid_a }),
|
||||
&cookie_a,
|
||||
))
|
||||
.await
|
||||
.unwrap()
|
||||
.status()
|
||||
});
|
||||
let f2 = tokio::spawn(async move {
|
||||
app_b
|
||||
.oneshot(common::post_json_with_cookie(
|
||||
"/api/v1/bookmarks",
|
||||
json!({ "manga_id": mid_b }),
|
||||
&cookie_b,
|
||||
))
|
||||
.await
|
||||
.unwrap()
|
||||
.status()
|
||||
});
|
||||
|
||||
let (s1, s2) = tokio::join!(f1, f2);
|
||||
let statuses = [s1.unwrap(), s2.unwrap()];
|
||||
assert!(
|
||||
statuses.contains(&StatusCode::CREATED),
|
||||
"expected one winner with 201, got {statuses:?}"
|
||||
);
|
||||
assert!(
|
||||
statuses.contains(&StatusCode::CONFLICT),
|
||||
"expected one loser with 409 (the partial unique index), got {statuses:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[sqlx::test(migrations = "./migrations")]
|
||||
async fn bookmark_create_accepts_bearer_token(pool: PgPool) {
|
||||
// Bot scripts use Authorization: Bearer; cover that path on a
|
||||
// *write* endpoint to make sure CurrentUser resolves identically
|
||||
// whether the credential is a cookie or a bearer.
|
||||
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/auth/tokens",
|
||||
json!({ "name": "ci-bot" }),
|
||||
&cookie,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
let bearer = common::body_json(resp).await["bearer"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
let resp = h
|
||||
.app
|
||||
.oneshot(common::post_json_with_bearer(
|
||||
"/api/v1/bookmarks",
|
||||
json!({ "manga_id": manga_id.to_string() }),
|
||||
&bearer,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::CREATED);
|
||||
}
|
||||
|
||||
#[sqlx::test(migrations = "./migrations")]
|
||||
async fn delete_unknown_bookmark_is_404(pool: PgPool) {
|
||||
let h = common::harness(pool);
|
||||
|
||||
Reference in New Issue
Block a user