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
+43 -72
View File
@@ -1,4 +1,5 @@
use std::collections::HashMap;
mod common;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use std::time::{Duration, Instant};
@@ -9,65 +10,15 @@ use serde_json::json;
use tower::util::ServiceExt;
use doctate_common::API_KEY_HEADER;
use doctate_server::config::{Config, User};
use doctate_server::magic_link::{MagicLinkStore, PendingMagicLink};
use doctate_server::{
AnalyzeBusy, AppState, OnelinerHealBusy, TranscribeBusy, analyze, transcribe, web_session,
};
use common::{TestConfig, body_json, test_user};
const API_KEY: &str = "key-dr_a";
fn make_user() -> User {
User {
slug: "dr_a".into(),
api_key: API_KEY.into(),
web_password: bcrypt::hash("unused", 4).unwrap(),
role: "doctor".into(),
whisper: Default::default(),
retention: Default::default(),
window_hours: 72,
preview_lines: 2,
}
}
fn build_state() -> (AppState, Arc<Config>) {
let user = make_user();
let data_path = std::env::temp_dir().join(format!(
"doctate-magic-test-{}-{}",
std::process::id(),
uuid::Uuid::new_v4()
));
let mut api_keys: HashMap<String, String> = HashMap::new();
api_keys.insert(user.api_key.clone(), user.slug.clone());
let config = Arc::new(Config {
data_path,
users: vec![user],
api_keys,
..Config::test_default()
});
let (transcribe_tx, _transcribe_rx) = transcribe::channel();
let (analyze_tx, _analyze_rx) = analyze::channel();
let magic_link_store = doctate_server::magic_link::new_store();
let state = AppState {
config: config.clone(),
transcribe_tx,
analyze_tx,
session_store: web_session::new_store(),
magic_link_store,
analyze_busy: AnalyzeBusy(Arc::new(AtomicBool::new(false))),
transcribe_busy: TranscribeBusy(Arc::new(AtomicBool::new(false))),
oneliner_heal_busy: OnelinerHealBusy(Arc::new(AtomicBool::new(false))),
events_tx: doctate_server::events::channel(),
http_client: reqwest::Client::new(),
vocab: Arc::new(doctate_server::gazetteer::Gazetteer::empty()),
};
(state, config)
}
fn create_request(body: &str, api_key: Option<&str>) -> Request<Body> {
let mut b = Request::builder()
.method("POST")
@@ -82,32 +33,55 @@ fn create_request(body: &str, api_key: Option<&str>) -> Request<Body> {
fn consume_request(token: &str) -> Request<Body> {
Request::builder()
.method("GET")
.uri(format!("/web/magic?token={token}"))
.uri(common::paths::magic(token))
.body(Body::empty())
.unwrap()
}
async fn body_json(response: axum::response::Response) -> serde_json::Value {
let bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
serde_json::from_slice(&bytes).unwrap()
/// Most tests only need a router. Only the expiry test keeps a handle
/// to the `MagicLinkStore` so it can insert a pre-expired token.
fn test_app() -> axum::Router {
let cfg = TestConfig::new()
.with_label("magic")
.with_user(test_user("dr_a"))
.build();
doctate_server::create_router(cfg)
}
/// For the expiry test: build the full `AppState` so we retain a
/// reference to `magic_link_store` after the router is constructed.
fn build_state_with_stores() -> AppState {
let cfg = TestConfig::new()
.with_label("magic")
.with_user(test_user("dr_a"))
.build();
let (transcribe_tx, _transcribe_rx) = transcribe::channel();
let (analyze_tx, _analyze_rx) = analyze::channel();
AppState {
config: cfg,
transcribe_tx,
analyze_tx,
session_store: web_session::new_store(),
magic_link_store: doctate_server::magic_link::new_store(),
analyze_busy: AnalyzeBusy(Arc::new(AtomicBool::new(false))),
transcribe_busy: TranscribeBusy(Arc::new(AtomicBool::new(false))),
oneliner_heal_busy: OnelinerHealBusy(Arc::new(AtomicBool::new(false))),
events_tx: doctate_server::events::channel(),
http_client: reqwest::Client::new(),
vocab: Arc::new(doctate_server::gazetteer::Gazetteer::empty()),
}
}
#[tokio::test]
async fn create_without_api_key_returns_401() {
let (state, _) = build_state();
let app = doctate_server::create_router_with_state(state);
let app = test_app();
let resp = app.oneshot(create_request("{}", None)).await.unwrap();
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn create_with_valid_key_returns_token() {
let (state, _) = build_state();
let app = doctate_server::create_router_with_state(state);
let app = test_app();
let resp = app
.oneshot(create_request(
r#"{"return_to":"/web/cases/abc"}"#,
@@ -124,8 +98,7 @@ async fn create_with_valid_key_returns_token() {
#[tokio::test]
async fn consume_valid_token_sets_cookie_and_redirects() {
let (state, _) = build_state();
let app = doctate_server::create_router_with_state(state);
let app = test_app();
// Step 1: issue token via API.
let create_resp = app
@@ -175,8 +148,7 @@ async fn consume_valid_token_sets_cookie_and_redirects() {
#[tokio::test]
async fn consume_token_is_one_time_use() {
let (state, _) = build_state();
let app = doctate_server::create_router_with_state(state);
let app = test_app();
let token = body_json(
app.clone()
@@ -207,8 +179,7 @@ async fn consume_token_is_one_time_use() {
#[tokio::test]
async fn create_rejects_open_redirect_return_to() {
let (state, _) = build_state();
let app = doctate_server::create_router_with_state(state);
let app = test_app();
for evil in [
"https://evil.com/",
@@ -232,7 +203,7 @@ async fn create_rejects_open_redirect_return_to() {
#[tokio::test]
async fn consume_expired_token_redirects_to_login() {
let (state, _) = build_state();
let state = build_state_with_stores();
let store: MagicLinkStore = state.magic_link_store.clone();
// Insert a token that expired 1s ago — bypassing the API to control time.