Files
doctate/server/tests/common/session.rs
T
Brummel 2c6062a53e refactor: drop /web/ URL prefix from browser routes
The /web/ prefix predated the /api/ split; today it just clutters every URL
without disambiguating anything. All 16 browser routes move to the apex
(/cases, /login, /magic, /events, /audio/...). The 6 /api/* routes are
unchanged. A new /->>/cases redirect closes the apex 404.

The open-redirect guard in magic.rs and case_actions.rs flips from a
positive whitelist (starts_with("/web/")) to a deny-list: same-origin path,
not protocol-relative, not under /api/, no \. The /api/ exclusion is now
load-bearing and covered by tests.

Pre-production: no transition redirects.
2026-05-04 18:36:10 +02:00

85 lines
2.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Session cookie extraction + login flows.
//!
//! These were previously copy-pasted 46 times across the integration
//! tests. Centralising them here means: a change to the cookie format
//! or the login form field names affects one file, not six.
use axum::Router;
use axum::body::Body;
use axum::http::{Request, header};
use axum::response::Response;
use tower::util::ServiceExt;
use doctate_server::web_session::SessionStore;
use crate::common::paths;
/// Extract the freshly-minted `session=<value>` cookie from a login
/// response's `Set-Cookie` headers. Returns the whole `name=value` pair
/// (no attributes like `; Path=/`) so it can be sent back verbatim in a
/// `Cookie` header.
pub fn extract_session_cookie(resp: &Response) -> Option<String> {
for v in resp.headers().get_all(header::SET_COOKIE).iter() {
let s = v.to_str().ok()?;
if let Some(pair) = s.split(';').next()
&& pair.starts_with("session=")
{
return Some(pair.to_string());
}
}
None
}
/// Build the `POST /login` request with a URL-encoded form body.
/// Pulled out so tests that need to *inspect* the login response
/// (e.g. session-fixation tests) can reuse the exact wire shape.
pub fn login_request(slug: &str, password: &str) -> Request<Body> {
let body = format!("slug={slug}&password={password}");
Request::builder()
.method("POST")
.uri(paths::LOGIN)
.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
.body(Body::from(body))
.unwrap()
}
/// Log in, return the resulting `session=...` cookie string.
///
/// Panics if the server didn't set a session cookie — meaning the
/// credentials were wrong or the route shape has changed. Tests that
/// deliberately exercise the failure path should use [`login_request`]
/// directly.
pub async fn login(app: &Router, slug: &str, password: &str) -> String {
let resp = app
.clone()
.oneshot(login_request(slug, password))
.await
.unwrap();
extract_session_cookie(&resp).expect("login did not set a session cookie")
}
/// Log in and additionally fetch the session's CSRF token directly from
/// the session store. Pairs with
/// `doctate_server::create_router_and_session_store` — the returned
/// token is what the `CsrfForm<T>` extractor validates on every
/// state-changing POST under `/`.
///
/// Returns `(cookie_header_value, csrf_token)`.
pub async fn login_with_csrf(
app: &Router,
store: &SessionStore,
slug: &str,
password: &str,
) -> (String, String) {
let cookie = login(app, slug, password).await;
let token = cookie.trim_start_matches("session=");
let csrf = store
.read()
.await
.get(token)
.expect("session must be in the store right after login")
.csrf_token
.clone();
(cookie, csrf)
}