Files
doctate/server/tests/common/session.rs
T
Brummel f438e972d4 refactor(tests): introduce shared tests/common/ support module
Lift duplicated test-harness code (User factories, TestConfig builder,
login/CSRF flow, form/json helpers, case-directory seeding, URL path
builders) into `server/tests/common/` so future API changes touch one
file instead of every integration test.

Migrated batches: csrf_attack, auth, login, magic_link, security_headers
(28 tests); analyze, case_page, delete_recording, close_watermark,
retention_sweep (67 tests). All 95 tests still green.

Net line delta across these 10 files: +1113 / -1896 (~780 lines removed),
plus ~500 lines of new shared infrastructure under tests/common/.
2026-04-22 10:42:51 +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 /web/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 `/web/`.
///
/// 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)
}