Files
doctate/server/tests/common/config.rs
T
Brummel a510c20e75 refactor(server): plumb Arc<Settings> through workers and routes
Config sheds its 14 Bucket-B fields (retention, whisper, ollama, llm) and
gains a sibling Arc<Settings> in AppState, loaded from settings.toml at
startup via Settings::load_or_default. Workers (transcribe, analyze,
recovery, auto_trigger) and routes (health, bulk, case_actions,
user_web) take settings as a separate parameter or State<> extractor;
Config::llm_configured moves to Settings::llm_configured.

TestConfig::build_pair() returns (Arc<Config>, Arc<Settings>); the new
create_router_with_settings / create_router_and_session_store_with_settings
helpers wire both into integration tests that exercise LLM/Whisper/Ollama.
The legacy single-Arc create_router falls back to Settings::default()
so the 17 non-LLM tests stay untouched.

Verified: cargo build clean, clippy --all-targets clean, 318 tests pass.
2026-04-27 11:48:41 +02:00

182 lines
5.6 KiB
Rust

//! `Config` construction for tests.
//!
//! Collapses the ~13 lookalike `test_config_*` / `build_config` helpers
//! scattered across the integration tests into one fluent builder.
//! Every builder call is a minimal override on top of
//! `Config::test_default()`; the builder automatically derives the
//! `api_key → slug` index map from the collected users.
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use doctate_server::config::{Config, User};
use doctate_server::settings::Settings;
/// Unique tempdir path for a given label. `process::id()` + UUID v4
/// keeps parallel `cargo test` runs from colliding; the label lets
/// humans identify which test owns which leftover directory when
/// cleanup fails.
pub fn unique_tmpdir(label: &str) -> PathBuf {
std::env::temp_dir().join(format!(
"doctate-test-{label}-{}-{}",
std::process::id(),
uuid::Uuid::new_v4()
))
}
/// Fluent builder for `Arc<Config>`. All methods take `self` by value
/// and return `Self` so calls chain naturally:
///
/// ```ignore
/// let cfg = TestConfig::new()
/// .with_user(test_user("dr_a"))
/// .with_llm(mock.uri())
/// .build();
/// ```
pub struct TestConfig {
data_path: Option<PathBuf>,
users: Vec<User>,
llm_url: Option<String>,
llm_api_key: Option<String>,
llm_model: Option<String>,
whisper_url: Option<String>,
whisper_timeout_seconds: Option<u64>,
ollama_url: Option<String>,
label: &'static str,
}
impl TestConfig {
/// Start a fresh builder. `label` is used only to name the tempdir
/// when no explicit `data_path` is set via `with_data_path`.
pub fn new() -> Self {
Self {
data_path: None,
users: Vec::new(),
llm_url: None,
llm_api_key: None,
llm_model: None,
whisper_url: None,
whisper_timeout_seconds: None,
ollama_url: None,
label: "common",
}
}
/// Label used when auto-generating the tempdir name.
pub fn with_label(mut self, label: &'static str) -> Self {
self.label = label;
self
}
/// Override the data path. By default `build()` generates a unique
/// tempdir under `std::env::temp_dir()`.
pub fn with_data_path(mut self, path: PathBuf) -> Self {
self.data_path = Some(path);
self
}
/// Add a single user. Can be called repeatedly.
pub fn with_user(mut self, user: User) -> Self {
self.users.push(user);
self
}
/// Replace the user list outright.
pub fn with_users(mut self, users: Vec<User>) -> Self {
self.users = users;
self
}
/// Configure a hosted-provider LLM with the common test defaults
/// (`api_key = "test-key"`, `model = "test-model"`). For Ollama-style
/// endpoints that expect no `Authorization` header, use
/// [`with_llm_explicit`] and pass an empty api_key.
pub fn with_llm(mut self, url: impl Into<String>) -> Self {
self.llm_url = Some(url.into());
self.llm_api_key.get_or_insert_with(|| "test-key".into());
self.llm_model.get_or_insert_with(|| "test-model".into());
self
}
/// Configure the LLM with explicit values. `api_key` may be empty
/// to exercise the Ollama-style no-auth path.
pub fn with_llm_explicit(
mut self,
url: impl Into<String>,
api_key: impl Into<String>,
model: impl Into<String>,
) -> Self {
self.llm_url = Some(url.into());
self.llm_api_key = Some(api_key.into());
self.llm_model = Some(model.into());
self
}
/// Whisper ASR mock URL + timeout.
pub fn with_whisper(mut self, url: impl Into<String>, timeout_seconds: u64) -> Self {
self.whisper_url = Some(url.into());
self.whisper_timeout_seconds = Some(timeout_seconds);
self
}
/// Ollama (oneliner generator) mock URL.
pub fn with_ollama(mut self, url: impl Into<String>) -> Self {
self.ollama_url = Some(url.into());
self
}
/// Materialize the config alone. Bucket-B overrides (`with_llm`,
/// `with_whisper`, `with_ollama`) are silently dropped — call
/// [`build_pair`] when those need to take effect.
pub fn build(self) -> Arc<Config> {
self.build_pair().0
}
/// Materialize both `Config` and `Settings`. Use when a test exercises
/// LLM, Whisper or Ollama paths and needs the builder-set values to
/// reach the worker.
pub fn build_pair(self) -> (Arc<Config>, Arc<Settings>) {
let data_path = self.data_path.unwrap_or_else(|| unique_tmpdir(self.label));
let api_keys: HashMap<String, String> = self
.users
.iter()
.map(|u| (u.api_key.clone(), u.slug.clone()))
.collect();
let config = Arc::new(Config {
data_path,
users: self.users,
api_keys,
..Config::test_default()
});
let mut settings = Settings::default();
if let Some(v) = self.llm_url {
settings.llm.url = v;
}
if let Some(v) = self.llm_api_key {
settings.llm.api_key = v;
}
if let Some(v) = self.llm_model {
settings.llm.model = v;
}
if let Some(v) = self.whisper_url {
settings.whisper.url = v;
}
if let Some(v) = self.whisper_timeout_seconds {
settings.whisper.timeout_seconds = v;
}
if let Some(v) = self.ollama_url {
settings.ollama.url = v;
}
(config, Arc::new(settings))
}
}
impl Default for TestConfig {
fn default() -> Self {
Self::new()
}
}