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,95 @@
|
||||
//! Filesystem-fixture builders for case directories, recordings,
|
||||
//! oneliner state files, and close markers.
|
||||
//!
|
||||
//! All functions use synchronous `std::fs` (tests run inside
|
||||
//! `#[tokio::test]` but these writes are short and their output is
|
||||
//! consumed sequentially, so there is no win from async I/O here). A
|
||||
//! few of the oneliner_api tests use `tokio::fs` directly because they
|
||||
//! need to run inside the same async flow; that's fine — the two
|
||||
//! coexist.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use doctate_common::oneliners::OnelinerState;
|
||||
use filetime::FileTime;
|
||||
|
||||
/// Create `<data_path>/<slug>/<case_id>/` and return its path. This is
|
||||
/// the only "has to be on disk for the route to see it" fixture —
|
||||
/// recordings and derived artefacts layer on top.
|
||||
pub fn seed_case(data_path: &Path, slug: &str, case_id: &str) -> PathBuf {
|
||||
let dir = data_path.join(slug).join(case_id);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
dir
|
||||
}
|
||||
|
||||
/// Write `<stem>.m4a` with placeholder bytes, plus optionally a
|
||||
/// `<stem>.transcript.txt`. `stem` must be the full RFC3339-like
|
||||
/// filename stem (e.g. `2026-04-15T10-00-00Z`); pass the same prefix
|
||||
/// you want to read back later. Returns the audio filename (no path)
|
||||
/// so the caller can feed it back to the delete endpoint.
|
||||
pub fn seed_recording(case_dir: &Path, stem: &str, transcript: Option<&str>) -> String {
|
||||
let filename = format!("{stem}.m4a");
|
||||
std::fs::write(case_dir.join(&filename), b"audio-bytes").unwrap();
|
||||
if let Some(text) = transcript {
|
||||
std::fs::write(case_dir.join(format!("{stem}.transcript.txt")), text).unwrap();
|
||||
}
|
||||
filename
|
||||
}
|
||||
|
||||
/// Like [`seed_recording`] but additionally writes the `.duration.txt`
|
||||
/// sidecar — used by the delete-recording tests that assert every
|
||||
/// sidecar is cleaned up.
|
||||
pub fn seed_recording_with_sidecars(
|
||||
case_dir: &Path,
|
||||
stem: &str,
|
||||
transcript: &str,
|
||||
) -> String {
|
||||
let filename = seed_recording(case_dir, stem, Some(transcript));
|
||||
std::fs::write(case_dir.join(format!("{stem}.duration.txt")), "42").unwrap();
|
||||
filename
|
||||
}
|
||||
|
||||
/// Write an m4a with an artificial `mtime` in the past (relative to
|
||||
/// now). Used by the retention-sweep tests to simulate "recorded N days
|
||||
/// ago" without a clock abstraction.
|
||||
pub fn seed_recording_with_age(case_dir: &Path, stem: &str, age: Duration) {
|
||||
let filename = format!("{stem}.m4a");
|
||||
let path = case_dir.join(&filename);
|
||||
std::fs::write(&path, b"audio").unwrap();
|
||||
let target = SystemTime::now() - age;
|
||||
filetime::set_file_mtime(&path, FileTime::from_system_time(target)).unwrap();
|
||||
}
|
||||
|
||||
/// Persist `OnelinerState::Ready { text }` with a fixed `generated_at`.
|
||||
/// Used by UI-rendering tests that care about the displayed text, not
|
||||
/// the timestamp.
|
||||
pub fn seed_oneliner_ready(case_dir: &Path, text: &str) {
|
||||
let state = OnelinerState::Ready {
|
||||
text: text.to_owned(),
|
||||
generated_at: "2026-04-18T10:00:00Z".into(),
|
||||
};
|
||||
seed_oneliner_state(case_dir, &state);
|
||||
}
|
||||
|
||||
/// Persist an arbitrary [`OnelinerState`] as `oneliner.json`.
|
||||
pub fn seed_oneliner_state(case_dir: &Path, state: &OnelinerState) {
|
||||
let bytes = serde_json::to_vec(state).unwrap();
|
||||
std::fs::write(case_dir.join("oneliner.json"), bytes).unwrap();
|
||||
}
|
||||
|
||||
/// Write a `.closed` marker with a caller-chosen `closed_at` string.
|
||||
/// Tests that need a specific age pass a historical RFC3339 stamp; a
|
||||
/// helper for that is [`past_rfc3339`].
|
||||
pub fn write_closed_marker(case_dir: &Path, closed_at: &str) {
|
||||
let json = format!(r#"{{"closed_at":"{closed_at}"}}"#);
|
||||
std::fs::write(case_dir.join(".closed"), json).unwrap();
|
||||
}
|
||||
|
||||
/// Format "now - age" as RFC3339 UTC. Used to mint historical
|
||||
/// `closed_at` strings for retention-purge tests.
|
||||
pub fn past_rfc3339(age: Duration) -> String {
|
||||
use time::format_description::well_known::Rfc3339;
|
||||
let t = time::OffsetDateTime::now_utc() - time::Duration::seconds(age.as_secs() as i64);
|
||||
t.format(&Rfc3339).unwrap()
|
||||
}
|
||||
Reference in New Issue
Block a user