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:
@@ -0,0 +1,84 @@
|
||||
//! 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=<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)
|
||||
}
|
||||
Reference in New Issue
Block a user