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:
@@ -48,7 +48,7 @@ pub fn router(state: AppState) -> Router {
|
||||
.layer(TraceLayer::new_for_http())
|
||||
}
|
||||
|
||||
fn cors_layer(allowed_origins: &[String]) -> CorsLayer {
|
||||
pub(crate) fn cors_layer(allowed_origins: &[String]) -> CorsLayer {
|
||||
if allowed_origins.is_empty() {
|
||||
// Same-origin only — no CORS headers emitted.
|
||||
return CorsLayer::new();
|
||||
@@ -66,3 +66,80 @@ fn cors_layer(allowed_origins: &[String]) -> CorsLayer {
|
||||
HeaderName::from_static("authorization"),
|
||||
])
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use axum::body::Body;
|
||||
use axum::http::Request;
|
||||
use axum::routing::get;
|
||||
use tower::ServiceExt;
|
||||
|
||||
fn test_router() -> Router {
|
||||
Router::new().route("/", get(|| async { "ok" }))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn allowlist_preflight_emits_credentialed_headers() {
|
||||
let app = test_router().layer(cors_layer(&["https://app.example.com".to_string()]));
|
||||
let resp = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method(Method::OPTIONS)
|
||||
.uri("/")
|
||||
.header("origin", "https://app.example.com")
|
||||
.header("access-control-request-method", "POST")
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
resp.headers().get("access-control-allow-origin").unwrap(),
|
||||
"https://app.example.com"
|
||||
);
|
||||
assert_eq!(
|
||||
resp.headers().get("access-control-allow-credentials").unwrap(),
|
||||
"true"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn allowlist_rejects_unlisted_origin() {
|
||||
let app = test_router().layer(cors_layer(&["https://app.example.com".to_string()]));
|
||||
let resp = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method(Method::OPTIONS)
|
||||
.uri("/")
|
||||
.header("origin", "https://evil.example.org")
|
||||
.header("access-control-request-method", "POST")
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
// Browsers will refuse the response when the allow-origin header
|
||||
// is absent (or doesn't echo the requesting origin).
|
||||
assert!(resp.headers().get("access-control-allow-origin").is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn empty_allowlist_is_same_origin_only() {
|
||||
let app = test_router().layer(cors_layer(&[]));
|
||||
let resp = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method(Method::OPTIONS)
|
||||
.uri("/")
|
||||
.header("origin", "https://app.example.com")
|
||||
.header("access-control-request-method", "POST")
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(resp.headers().get("access-control-allow-origin").is_none());
|
||||
assert!(resp.headers().get("access-control-allow-credentials").is_none());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user