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:
+52
-138
@@ -1,77 +1,20 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
mod common;
|
||||
|
||||
use axum::body::Body;
|
||||
use axum::http::{Request, StatusCode, header};
|
||||
use tower::util::ServiceExt;
|
||||
|
||||
use doctate_server::config::{Config, User};
|
||||
|
||||
fn make_user(slug: &str, password_plain: &str) -> User {
|
||||
User {
|
||||
slug: slug.into(),
|
||||
api_key: format!("key-{slug}"),
|
||||
web_password: bcrypt::hash(password_plain, 4).unwrap(),
|
||||
role: "doctor".into(),
|
||||
whisper: Default::default(),
|
||||
retention: Default::default(),
|
||||
window_hours: 72,
|
||||
preview_lines: 2,
|
||||
}
|
||||
}
|
||||
|
||||
fn test_config_with_users(users: Vec<User>) -> Arc<Config> {
|
||||
let data_path = std::env::temp_dir().join(format!(
|
||||
"doctate-login-test-{}-{}",
|
||||
std::process::id(),
|
||||
uuid::Uuid::new_v4()
|
||||
));
|
||||
let api_keys: HashMap<String, String> = users
|
||||
.iter()
|
||||
.map(|u| (u.api_key.clone(), u.slug.clone()))
|
||||
.collect();
|
||||
Arc::new(Config {
|
||||
data_path,
|
||||
users,
|
||||
api_keys,
|
||||
..Config::test_default()
|
||||
})
|
||||
}
|
||||
|
||||
async fn body_to_string(response: axum::response::Response) -> String {
|
||||
let bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.unwrap();
|
||||
String::from_utf8(bytes.to_vec()).unwrap()
|
||||
}
|
||||
|
||||
/// Extract the session cookie (name=value) from a Set-Cookie response.
|
||||
fn extract_session_cookie(resp: &axum::response::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
|
||||
}
|
||||
|
||||
fn login_request(slug: &str, password: &str) -> Request<Body> {
|
||||
let body = format!("slug={slug}&password={password}");
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/web/login")
|
||||
.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
|
||||
.body(Body::from(body))
|
||||
.unwrap()
|
||||
}
|
||||
use common::{
|
||||
TestConfig, body_string, extract_session_cookie, get_with_cookie, login_request, paths,
|
||||
test_user_with_password,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn login_success_sets_session_cookie_and_redirects() {
|
||||
let config = test_config_with_users(vec![make_user("dr_a", "secret")]);
|
||||
let app = doctate_server::create_router(config);
|
||||
let cfg = TestConfig::new()
|
||||
.with_user(test_user_with_password("dr_a", "secret"))
|
||||
.build();
|
||||
let app = doctate_server::create_router(cfg);
|
||||
|
||||
let resp = app.oneshot(login_request("dr_a", "secret")).await.unwrap();
|
||||
assert_eq!(
|
||||
@@ -99,20 +42,24 @@ async fn login_success_sets_session_cookie_and_redirects() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn login_wrong_password_shows_error() {
|
||||
let config = test_config_with_users(vec![make_user("dr_a", "secret")]);
|
||||
let app = doctate_server::create_router(config);
|
||||
let cfg = TestConfig::new()
|
||||
.with_user(test_user_with_password("dr_a", "secret"))
|
||||
.build();
|
||||
let app = doctate_server::create_router(cfg);
|
||||
|
||||
let resp = app.oneshot(login_request("dr_a", "wrong")).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert!(extract_session_cookie(&resp).is_none());
|
||||
let body = body_to_string(resp).await;
|
||||
let body = body_string(resp).await;
|
||||
assert!(body.contains("Login fehlgeschlagen"), "got: {body}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn login_unknown_user_shows_error() {
|
||||
let config = test_config_with_users(vec![make_user("dr_a", "secret")]);
|
||||
let app = doctate_server::create_router(config);
|
||||
let cfg = TestConfig::new()
|
||||
.with_user(test_user_with_password("dr_a", "secret"))
|
||||
.build();
|
||||
let app = doctate_server::create_router(cfg);
|
||||
|
||||
let resp = app
|
||||
.oneshot(login_request("ghost", "anything"))
|
||||
@@ -124,13 +71,15 @@ async fn login_unknown_user_shows_error() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn cases_without_cookie_redirects_to_login() {
|
||||
let config = test_config_with_users(vec![make_user("dr_a", "secret")]);
|
||||
let app = doctate_server::create_router(config);
|
||||
let cfg = TestConfig::new()
|
||||
.with_user(test_user_with_password("dr_a", "secret"))
|
||||
.build();
|
||||
let app = doctate_server::create_router(cfg);
|
||||
|
||||
let resp = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/web/cases")
|
||||
.uri(paths::CASES)
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
@@ -143,18 +92,21 @@ async fn cases_without_cookie_redirects_to_login() {
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap(),
|
||||
"/web/login"
|
||||
paths::LOGIN
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cases_with_valid_cookie_shows_only_own_cases() {
|
||||
let config = test_config_with_users(vec![make_user("dr_a", "s"), make_user("dr_b", "s")]);
|
||||
let cfg = TestConfig::new()
|
||||
.with_user(test_user_with_password("dr_a", "s"))
|
||||
.with_user(test_user_with_password("dr_b", "s"))
|
||||
.build();
|
||||
// Seed fixtures: dr_a has an open case, dr_b has one too.
|
||||
let case_a = config
|
||||
let case_a = cfg
|
||||
.data_path
|
||||
.join("dr_a/11111111-1111-1111-1111-111111111111");
|
||||
let case_b = config
|
||||
let case_b = cfg
|
||||
.data_path
|
||||
.join("dr_b/22222222-2222-2222-2222-222222222222");
|
||||
std::fs::create_dir_all(&case_a).unwrap();
|
||||
@@ -162,58 +114,39 @@ async fn cases_with_valid_cookie_shows_only_own_cases() {
|
||||
std::fs::write(case_a.join("2026-04-14T10-00-00Z.m4a"), b"x").unwrap();
|
||||
std::fs::write(case_b.join("2026-04-14T11-00-00Z.m4a"), b"x").unwrap();
|
||||
|
||||
let app = doctate_server::create_router(config);
|
||||
|
||||
// Log in as dr_a.
|
||||
let login = app
|
||||
.clone()
|
||||
.oneshot(login_request("dr_a", "s"))
|
||||
.await
|
||||
.unwrap();
|
||||
let cookie = extract_session_cookie(&login).unwrap();
|
||||
let app = doctate_server::create_router(cfg);
|
||||
let cookie = common::login(&app, "dr_a", "s").await;
|
||||
|
||||
let resp = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/web/cases")
|
||||
.header(header::COOKIE, &cookie)
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.oneshot(get_with_cookie(paths::CASES, &cookie))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
let body = body_to_string(resp).await;
|
||||
let body = body_string(resp).await;
|
||||
assert!(body.contains("11111111"), "own case missing: {body}");
|
||||
assert!(!body.contains("22222222"), "foreign case leaked: {body}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn case_detail_of_foreign_case_returns_404() {
|
||||
let config = test_config_with_users(vec![make_user("dr_a", "s"), make_user("dr_b", "s")]);
|
||||
let case_b = config
|
||||
let cfg = TestConfig::new()
|
||||
.with_user(test_user_with_password("dr_a", "s"))
|
||||
.with_user(test_user_with_password("dr_b", "s"))
|
||||
.build();
|
||||
let case_b = cfg
|
||||
.data_path
|
||||
.join("dr_b/22222222-2222-2222-2222-222222222222");
|
||||
std::fs::create_dir_all(&case_b).unwrap();
|
||||
std::fs::write(case_b.join("2026-04-14T11-00-00Z.m4a"), b"x").unwrap();
|
||||
|
||||
let app = doctate_server::create_router(config);
|
||||
|
||||
let login = app
|
||||
.clone()
|
||||
.oneshot(login_request("dr_a", "s"))
|
||||
.await
|
||||
.unwrap();
|
||||
let cookie = extract_session_cookie(&login).unwrap();
|
||||
let app = doctate_server::create_router(cfg);
|
||||
let cookie = common::login(&app, "dr_a", "s").await;
|
||||
|
||||
let resp = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/web/cases/22222222-2222-2222-2222-222222222222")
|
||||
.header(header::COOKIE, &cookie)
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.oneshot(get_with_cookie(
|
||||
paths::case_detail("22222222-2222-2222-2222-222222222222"),
|
||||
&cookie,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
||||
@@ -221,42 +154,23 @@ async fn case_detail_of_foreign_case_returns_404() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn logout_clears_session() {
|
||||
let config = test_config_with_users(vec![make_user("dr_a", "s")]);
|
||||
let (app, store) = doctate_server::create_router_and_session_store(config);
|
||||
let cfg = TestConfig::new()
|
||||
.with_user(test_user_with_password("dr_a", "s"))
|
||||
.build();
|
||||
let (app, store) = doctate_server::create_router_and_session_store(cfg);
|
||||
|
||||
let login = app
|
||||
.clone()
|
||||
.oneshot(login_request("dr_a", "s"))
|
||||
.await
|
||||
.unwrap();
|
||||
let cookie = extract_session_cookie(&login).unwrap();
|
||||
let token = cookie.trim_start_matches("session=");
|
||||
let csrf = store.read().await.get(token).unwrap().csrf_token.clone();
|
||||
let (cookie, csrf) = common::login_with_csrf(&app, &store, "dr_a", "s").await;
|
||||
|
||||
let logout = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/web/logout")
|
||||
.header(header::COOKIE, &cookie)
|
||||
.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
|
||||
.body(Body::from(format!("csrf_token={csrf}")))
|
||||
.unwrap(),
|
||||
)
|
||||
.oneshot(common::csrf_form_post(paths::LOGOUT, &cookie, &csrf, ""))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(logout.status(), StatusCode::SEE_OTHER.as_u16());
|
||||
|
||||
// Same cookie must no longer be accepted.
|
||||
let resp = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/web/cases")
|
||||
.header(header::COOKIE, &cookie)
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.oneshot(get_with_cookie(paths::CASES, &cookie))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::FOUND);
|
||||
|
||||
Reference in New Issue
Block a user