refactor(tests): migrate remaining batches to tests/common/

Finishes the lift of shared helpers into `tests/common/`. Covered:
- health, web, oneliners_api, upload (31 tests; upload exercises the
  new `multipart_upload_body` helper driven by doctate_common field
  constants)
- sse_integration, sse_cleanup, transcribe, oneliner_heal_decoupled,
  silent_case_empty, failed_only_case_empty_oneliner,
  transient_failure_retries (24 tests)

Also applied cargo fmt across the test tree and fixed one clippy
needless_borrows_for_generic_args warning in analyze_test.

All 387 tests pass; 3 ignored (as before).

Side effect: health_test previously used a hardcoded `/tmp/doctate-test`
data path, which parallel `cargo test` runs could collide on. The
migration replaces it with the common unique-tmpdir pattern, removing
a latent flake.
This commit is contained in:
2026-04-22 10:51:33 +02:00
parent f438e972d4
commit af3377a6bc
21 changed files with 374 additions and 793 deletions
+18 -89
View File
@@ -11,88 +11,31 @@
//! channel through an HTTP test would require reading a never-ending
//! body, which these tests deliberately avoid.
use std::collections::HashMap;
use std::path::PathBuf;
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};
use common::{TestConfig, get_with_cookie, header_opt, paths, test_user};
fn unique_data_path() -> PathBuf {
std::env::temp_dir().join(format!(
"doctate-sse-test-{}-{}",
std::process::id(),
uuid::Uuid::new_v4()
))
}
fn make_user(slug: &str) -> User {
User {
slug: slug.into(),
api_key: format!("key-{slug}"),
// Password "s" — matches the login helper below.
web_password: bcrypt::hash("s", 4).unwrap(),
role: "doctor".into(),
whisper: Default::default(),
retention: Default::default(),
window_hours: 72,
preview_lines: 2,
}
}
fn build_config(data_path: PathBuf) -> Arc<Config> {
let users = vec![make_user("alice")];
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()
})
}
/// Log in via POST /web/login and return the resulting `session=…` cookie
/// header value. Mirrors the helper in the other integration tests.
async fn login(app: axum::Router, slug: &str) -> String {
let body = format!("slug={slug}&password=s");
let resp = app
.oneshot(
Request::builder()
.method("POST")
.uri("/web/login")
.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
.body(Body::from(body))
.unwrap(),
)
.await
.unwrap();
for v in resp.headers().get_all(header::SET_COOKIE).iter() {
let s = v.to_str().unwrap();
if let Some(pair) = s.split(';').next()
&& pair.starts_with("session=")
{
return pair.to_string();
}
}
panic!("no session cookie after login");
fn test_app() -> axum::Router {
let cfg = TestConfig::new()
.with_label("sse")
.with_user(test_user("alice"))
.build();
doctate_server::create_router(cfg)
}
#[tokio::test]
async fn events_without_session_redirects_to_login() {
let config = build_config(unique_data_path());
let app = doctate_server::create_router(config);
let app = test_app();
let resp = app
.oneshot(
Request::builder()
.method("GET")
.uri("/web/events")
.uri(paths::EVENTS)
.body(Body::empty())
.unwrap(),
)
@@ -100,38 +43,24 @@ async fn events_without_session_redirects_to_login() {
.unwrap();
assert_eq!(resp.status(), StatusCode::FOUND);
let location = resp
.headers()
.get(header::LOCATION)
.and_then(|v| v.to_str().ok())
.unwrap_or("");
assert_eq!(location, "/web/login");
assert_eq!(
header_opt(&resp, header::LOCATION.as_str()),
Some(paths::LOGIN)
);
}
#[tokio::test]
async fn events_with_session_returns_sse_content_type() {
let config = build_config(unique_data_path());
let app = doctate_server::create_router(config);
let cookie = login(app.clone(), "alice").await;
let app = test_app();
let cookie = common::login(&app, "alice", "s").await;
let resp = app
.oneshot(
Request::builder()
.method("GET")
.uri("/web/events")
.header(header::COOKIE, cookie)
.body(Body::empty())
.unwrap(),
)
.oneshot(get_with_cookie(paths::EVENTS, &cookie))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let ct = resp
.headers()
.get(header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
.unwrap_or("");
let ct = header_opt(&resp, header::CONTENT_TYPE.as_str()).unwrap_or("");
assert!(
ct.starts_with("text/event-stream"),
"expected text/event-stream, got {ct:?}"