//! `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}; /// 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`. 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, users: Vec, llm_url: Option, llm_api_key: Option, llm_model: Option, whisper_url: Option, whisper_timeout_seconds: Option, ollama_url: Option, 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) -> 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) -> 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, api_key: impl Into, model: impl Into, ) -> 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, 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) -> Self { self.ollama_url = Some(url.into()); self } /// Materialize the config. Panics only if the workspace-defaults in /// `Config::test_default` themselves are broken (not reachable in /// practice). pub fn build(self) -> Arc { let data_path = self .data_path .unwrap_or_else(|| unique_tmpdir(self.label)); let api_keys: HashMap = self .users .iter() .map(|u| (u.api_key.clone(), u.slug.clone())) .collect(); let defaults = Config::test_default(); Arc::new(Config { data_path, users: self.users, api_keys, llm_url: self.llm_url.unwrap_or(defaults.llm_url), llm_api_key: self.llm_api_key.unwrap_or(defaults.llm_api_key), llm_model: self.llm_model.unwrap_or(defaults.llm_model), whisper_url: self.whisper_url.unwrap_or(defaults.whisper_url), whisper_timeout_seconds: self .whisper_timeout_seconds .unwrap_or(defaults.whisper_timeout_seconds), ollama_url: self.ollama_url.unwrap_or(defaults.ollama_url), ..Config::test_default() }) } } impl Default for TestConfig { fn default() -> Self { Self::new() } }