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/.
This commit is contained in:
2026-04-22 10:42:51 +02:00
parent 2e3b5efe86
commit f438e972d4
17 changed files with 1808 additions and 1898 deletions
+84
View File
@@ -0,0 +1,84 @@
//! 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)
}