//! Session cookie extraction + login flows. //! //! These were previously copy-pasted 4–6 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=` 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 { 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 { 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` 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) }