mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-04 02:33:56 +00:00
Keep typed transport and provider errors intact through API, GitHub, OAuth, install, diagnostics, and artifact paths. Add regression coverage for cloned shared errors and communication error chains.
642 lines
20 KiB
Rust
642 lines
20 KiB
Rust
use std::path::{Path, PathBuf};
|
|
use std::str::FromStr;
|
|
|
|
use anyhow::{Context, Result, anyhow};
|
|
pub(crate) use fabro_client::ServerTarget;
|
|
pub(crate) use fabro_config::user::{active_settings_path, default_storage_dir};
|
|
use fabro_config::user::{default_settings_path, default_socket_path};
|
|
use fabro_config::{
|
|
CliLayer, LogFilter, ParseError, RunSettingsBuilder, ServerSettingsBuilder, UserSettingsBuilder,
|
|
};
|
|
use fabro_static::EnvVars;
|
|
use fabro_types::settings::cli::CliTargetSettings;
|
|
use fabro_types::settings::server::LogDestination;
|
|
use fabro_types::settings::{CliNamespace, InterpString, RunNamespace};
|
|
use fabro_types::{ServerSettings, UserSettings};
|
|
use fabro_util::error::SharedError;
|
|
use fabro_util::version::FABRO_VERSION;
|
|
use toml_edit::{DocumentMut, Item, Table, value};
|
|
use tracing::debug;
|
|
|
|
use crate::args::ServerTargetArgs;
|
|
|
|
pub(crate) struct LoadedSettings {
|
|
pub(crate) storage_dir: PathBuf,
|
|
pub(crate) config_log_level: Option<LogFilter>,
|
|
pub(crate) config_log_destination: Option<LogDestination>,
|
|
pub(crate) run_settings: std::result::Result<RunNamespace, SharedError>,
|
|
pub(crate) server_settings: std::result::Result<ServerSettings, SharedError>,
|
|
pub(crate) user_settings: UserSettings,
|
|
}
|
|
|
|
pub(crate) fn load_resolved_settings(
|
|
config_path: Option<&Path>,
|
|
storage_dir: Option<&Path>,
|
|
cli_layer: Option<&CliLayer>,
|
|
) -> anyhow::Result<LoadedSettings> {
|
|
let document = load_settings_document(config_path)?;
|
|
let storage_override = storage_dir.map(Path::to_path_buf);
|
|
let storage_dir = storage_dir_from_document(&document, storage_dir)?;
|
|
let pre_tracing_config = pre_tracing_config_from_document(&document)?;
|
|
let run_settings = load_run_settings(config_path).map_err(SharedError::new);
|
|
let server_settings = load_server_settings(config_path)
|
|
.map(|settings| match storage_override.as_deref() {
|
|
Some(dir) => settings.with_storage_override(dir),
|
|
None => settings,
|
|
})
|
|
.map_err(SharedError::new);
|
|
let user_settings = load_user_settings(config_path, cli_layer)?;
|
|
|
|
Ok(LoadedSettings {
|
|
storage_dir,
|
|
config_log_level: pre_tracing_config.log_level,
|
|
config_log_destination: pre_tracing_config.log_destination,
|
|
run_settings,
|
|
server_settings,
|
|
user_settings,
|
|
})
|
|
}
|
|
|
|
fn load_settings_document(config_path: Option<&Path>) -> anyhow::Result<toml::Value> {
|
|
load_settings_document_with_lookup(config_path, process_env_var_os)
|
|
}
|
|
|
|
#[expect(
|
|
clippy::disallowed_methods,
|
|
reason = "sync settings load during CLI startup; not on a Tokio path"
|
|
)]
|
|
fn load_settings_document_with_lookup(
|
|
config_path: Option<&Path>,
|
|
lookup: impl Fn(&str) -> Option<std::ffi::OsString>,
|
|
) -> anyhow::Result<toml::Value> {
|
|
let config_path = config_path
|
|
.map(Path::to_path_buf)
|
|
.or_else(|| lookup(EnvVars::FABRO_CONFIG).map(PathBuf::from));
|
|
|
|
let path = if let Some(path) = config_path {
|
|
path
|
|
} else {
|
|
let default_path = default_settings_path();
|
|
if !default_path.is_file() {
|
|
return Ok(toml::Value::Table(toml::Table::new()));
|
|
}
|
|
default_path
|
|
};
|
|
|
|
let contents = std::fs::read_to_string(&path)
|
|
.map_err(|source| fabro_config::Error::read_file(&path, source))?;
|
|
let table: toml::Table = toml::from_str(&contents).map_err(|source| {
|
|
fabro_config::Error::parse_file(
|
|
"Failed to parse settings file",
|
|
&path,
|
|
ParseError::Toml(source.to_string()),
|
|
)
|
|
})?;
|
|
Ok(toml::Value::Table(table))
|
|
}
|
|
|
|
fn load_run_settings(config_path: Option<&Path>) -> anyhow::Result<RunNamespace> {
|
|
Ok(match config_path {
|
|
Some(path) => RunSettingsBuilder::load_from(path)?,
|
|
None => RunSettingsBuilder::load_default()?,
|
|
})
|
|
}
|
|
|
|
fn load_server_settings(config_path: Option<&Path>) -> anyhow::Result<ServerSettings> {
|
|
Ok(match config_path {
|
|
Some(path) => ServerSettingsBuilder::load_from(path)?,
|
|
None => ServerSettingsBuilder::load_default()?,
|
|
})
|
|
}
|
|
|
|
fn load_user_settings(
|
|
config_path: Option<&Path>,
|
|
cli_layer: Option<&CliLayer>,
|
|
) -> anyhow::Result<UserSettings> {
|
|
Ok(match (config_path, cli_layer) {
|
|
(Some(path), Some(cli_layer)) => {
|
|
UserSettingsBuilder::load_from_with_cli_overrides(path, cli_layer)?
|
|
}
|
|
(Some(path), None) => UserSettingsBuilder::load_from(path)?,
|
|
(None, Some(cli_layer)) => UserSettingsBuilder::load_default_with_cli_overrides(cli_layer)?,
|
|
(None, None) => UserSettingsBuilder::load_default()?,
|
|
})
|
|
}
|
|
|
|
struct PreTracingConfig {
|
|
log_level: Option<LogFilter>,
|
|
log_destination: Option<LogDestination>,
|
|
}
|
|
|
|
fn pre_tracing_config_from_document(document: &toml::Value) -> Result<PreTracingConfig> {
|
|
Ok(PreTracingConfig {
|
|
log_level: log_filter_at_path(document, &["server", "logging", "level"])?,
|
|
log_destination: log_destination_at_path(document, &["server", "logging", "destination"])?,
|
|
})
|
|
}
|
|
|
|
fn log_filter_at_path(document: &toml::Value, path: &[&str]) -> Result<Option<LogFilter>> {
|
|
let Some(value) = value_at_path(document, path) else {
|
|
return Ok(None);
|
|
};
|
|
let raw = value
|
|
.as_str()
|
|
.ok_or_else(|| anyhow!("{} must be a string", path.join(".")))?;
|
|
LogFilter::parse(raw)
|
|
.with_context(|| format!("invalid {} `{raw}`", path.join(".")))
|
|
.map(Some)
|
|
}
|
|
|
|
fn log_destination_at_path(
|
|
document: &toml::Value,
|
|
path: &[&str],
|
|
) -> Result<Option<LogDestination>> {
|
|
let Some(value) = value_at_path(document, path) else {
|
|
return Ok(None);
|
|
};
|
|
let raw = value
|
|
.as_str()
|
|
.ok_or_else(|| anyhow!("{} must be a string", path.join(".")))?;
|
|
raw.parse::<LogDestination>()
|
|
.with_context(|| {
|
|
format!(
|
|
"invalid {} `{raw}`; expected `file` or `stdout`",
|
|
path.join(".")
|
|
)
|
|
})
|
|
.map(Some)
|
|
}
|
|
|
|
fn storage_dir_from_document(
|
|
document: &toml::Value,
|
|
storage_dir: Option<&Path>,
|
|
) -> anyhow::Result<PathBuf> {
|
|
storage_dir_from_document_with_lookup(document, storage_dir, &process_env_var)
|
|
}
|
|
|
|
#[expect(
|
|
clippy::disallowed_methods,
|
|
reason = "CLI settings loading owns the process-env facade for interpolation."
|
|
)]
|
|
fn process_env_var(name: &str) -> Option<String> {
|
|
std::env::var(name).ok()
|
|
}
|
|
|
|
#[expect(
|
|
clippy::disallowed_methods,
|
|
reason = "CLI settings loading owns the process-env facade for config path lookup."
|
|
)]
|
|
fn process_env_var_os(name: &str) -> Option<std::ffi::OsString> {
|
|
std::env::var_os(name)
|
|
}
|
|
|
|
fn storage_dir_from_document_with_lookup(
|
|
document: &toml::Value,
|
|
storage_dir: Option<&Path>,
|
|
lookup: &dyn Fn(&str) -> Option<String>,
|
|
) -> anyhow::Result<PathBuf> {
|
|
if let Some(dir) = storage_dir {
|
|
return Ok(dir.to_path_buf());
|
|
}
|
|
|
|
let storage_root = string_at_path(document, &["server", "storage", "root"]).map_or_else(
|
|
|| InterpString::parse(&default_storage_dir().to_string_lossy()),
|
|
|root| InterpString::parse(&root),
|
|
);
|
|
let resolved_root = storage_root.resolve(lookup)?;
|
|
Ok(PathBuf::from(resolved_root.value))
|
|
}
|
|
|
|
fn string_at_path(document: &toml::Value, path: &[&str]) -> Option<String> {
|
|
value_at_path(document, path).and_then(|value| value.as_str().map(str::to_owned))
|
|
}
|
|
|
|
fn value_at_path<'a>(document: &'a toml::Value, path: &[&str]) -> Option<&'a toml::Value> {
|
|
let mut current = document;
|
|
for segment in path {
|
|
current = current.get(*segment)?;
|
|
}
|
|
Some(current)
|
|
}
|
|
|
|
/// Pull the resolved CLI target configuration out of `[cli.target]`.
|
|
/// Returns either an http(s) URL or a unix socket path.
|
|
fn cli_target_from_settings(settings: &CliNamespace) -> Option<String> {
|
|
let target = settings.target.as_ref()?;
|
|
match target {
|
|
CliTargetSettings::Http { url } => Some(url.as_source()),
|
|
CliTargetSettings::Unix { path } => Some(path.as_source()),
|
|
}
|
|
}
|
|
|
|
fn configured_server_target(settings: &UserSettings) -> Result<Option<ServerTarget>> {
|
|
let Some(value) = cli_target_from_settings(&settings.cli) else {
|
|
return Ok(None);
|
|
};
|
|
parse_server_target(&value).map(Some)
|
|
}
|
|
|
|
pub(crate) fn default_server_target() -> ServerTarget {
|
|
ServerTarget::unix_socket_path(default_socket_path()).expect("default socket path is absolute")
|
|
}
|
|
|
|
fn parse_server_target(value: &str) -> Result<ServerTarget> {
|
|
ServerTarget::from_str(value)
|
|
}
|
|
|
|
fn explicit_server_target(args: &ServerTargetArgs) -> Result<Option<ServerTarget>> {
|
|
args.as_deref().map(parse_server_target).transpose()
|
|
}
|
|
|
|
pub(crate) fn resolve_nondefault_server_target(
|
|
args: &ServerTargetArgs,
|
|
settings: &UserSettings,
|
|
) -> Result<Option<ServerTarget>> {
|
|
Ok(explicit_server_target(args)?.or(configured_server_target(settings)?))
|
|
}
|
|
|
|
pub(crate) fn resolve_server_target(
|
|
args: &ServerTargetArgs,
|
|
settings: &UserSettings,
|
|
) -> Result<ServerTarget> {
|
|
Ok(resolve_nondefault_server_target(args, settings)?.unwrap_or_else(default_server_target))
|
|
}
|
|
|
|
#[expect(
|
|
clippy::disallowed_methods,
|
|
reason = "CLI auth/login updates the user settings file synchronously after successful login."
|
|
)]
|
|
pub(crate) fn configure_cli_target_if_missing(
|
|
config_path: &Path,
|
|
settings: &UserSettings,
|
|
target: &ServerTarget,
|
|
) -> Result<bool> {
|
|
if settings.cli.target.is_some() {
|
|
return Ok(false);
|
|
}
|
|
|
|
let mut document = read_settings_document_for_write(config_path)?;
|
|
if document
|
|
.get("cli")
|
|
.and_then(Item::as_table)
|
|
.is_some_and(|cli| cli.get("target").is_some_and(|target| !target.is_none()))
|
|
{
|
|
return Ok(false);
|
|
}
|
|
|
|
if document.get("_version").is_none() {
|
|
document["_version"] = value(1);
|
|
}
|
|
|
|
let cli = ensure_document_table(&mut document, "cli")?;
|
|
cli.insert("target", Item::Table(cli_target_table(target)));
|
|
|
|
if let Some(parent) = config_path.parent() {
|
|
std::fs::create_dir_all(parent)
|
|
.with_context(|| format!("failed to create {}", parent.display()))?;
|
|
}
|
|
std::fs::write(config_path, document.to_string())
|
|
.with_context(|| format!("failed to write {}", config_path.display()))?;
|
|
|
|
Ok(true)
|
|
}
|
|
|
|
#[expect(
|
|
clippy::disallowed_methods,
|
|
reason = "CLI auth/login reads the user settings file synchronously before writing it."
|
|
)]
|
|
fn read_settings_document_for_write(config_path: &Path) -> Result<DocumentMut> {
|
|
if !config_path.exists() {
|
|
return Ok(DocumentMut::new());
|
|
}
|
|
|
|
let contents = std::fs::read_to_string(config_path)
|
|
.with_context(|| format!("failed to read {}", config_path.display()))?;
|
|
contents
|
|
.parse::<DocumentMut>()
|
|
.with_context(|| format!("failed to parse {}", config_path.display()))
|
|
}
|
|
|
|
fn ensure_document_table<'a>(document: &'a mut DocumentMut, key: &str) -> Result<&'a mut Table> {
|
|
if document.get(key).is_none() {
|
|
let mut table = Table::new();
|
|
table.set_implicit(true);
|
|
document[key] = Item::Table(table);
|
|
}
|
|
document[key]
|
|
.as_table_mut()
|
|
.ok_or_else(|| anyhow!("settings.toml [{key}] is not a table"))
|
|
}
|
|
|
|
fn cli_target_table(target: &ServerTarget) -> Table {
|
|
let mut table = Table::new();
|
|
if let Some(url) = target.as_http_url() {
|
|
table.insert("type", value("http"));
|
|
table.insert("url", value(url));
|
|
} else if let Some(path) = target.as_unix_socket_path() {
|
|
table.insert("type", value("unix"));
|
|
table.insert("path", value(path.display().to_string()));
|
|
}
|
|
table
|
|
}
|
|
|
|
pub(crate) fn exec_server_target(args: &ServerTargetArgs) -> Result<Option<ServerTarget>> {
|
|
let target = explicit_server_target(args)?;
|
|
debug!(?target, "Resolved exec server target");
|
|
Ok(target)
|
|
}
|
|
|
|
pub(crate) fn cli_http_client_builder() -> fabro_http::HttpClientBuilder {
|
|
fabro_http::HttpClientBuilder::new().user_agent(format!("fabro-cli/{FABRO_VERSION}"))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub(crate) fn load_resolved_settings_from_toml(
|
|
source: &str,
|
|
storage_dir: Option<&Path>,
|
|
cli_layer: Option<&CliLayer>,
|
|
) -> anyhow::Result<LoadedSettings> {
|
|
let document: toml::Value = toml::from_str(source).context("failed to parse settings file")?;
|
|
let storage_override = storage_dir.map(Path::to_path_buf);
|
|
let storage_dir = storage_dir_from_document(&document, storage_dir)?;
|
|
let pre_tracing_config = pre_tracing_config_from_document(&document)?;
|
|
let run_settings = RunSettingsBuilder::from_toml(source)
|
|
.map_err(|err| SharedError::new(anyhow::Error::new(err)));
|
|
let server_settings = ServerSettingsBuilder::from_toml(source)
|
|
.map(|settings| match storage_override.as_deref() {
|
|
Some(dir) => settings.with_storage_override(dir),
|
|
None => settings,
|
|
})
|
|
.map_err(|err| SharedError::new(anyhow::Error::new(err)));
|
|
let user_settings = match cli_layer {
|
|
Some(cli_layer) => UserSettingsBuilder::from_toml_with_cli_overrides(source, cli_layer)?,
|
|
None => UserSettingsBuilder::from_toml(source)?,
|
|
};
|
|
|
|
Ok(LoadedSettings {
|
|
storage_dir,
|
|
config_log_level: pre_tracing_config.log_level,
|
|
config_log_destination: pre_tracing_config.log_destination,
|
|
run_settings,
|
|
server_settings,
|
|
user_settings,
|
|
})
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::path::PathBuf;
|
|
|
|
use fabro_config::UserSettingsBuilder;
|
|
use fabro_config::user::default_storage_dir;
|
|
use fabro_types::UserSettings;
|
|
|
|
use super::*;
|
|
use crate::args::ServerTargetArgs;
|
|
|
|
fn server_target_args(value: Option<&str>) -> ServerTargetArgs {
|
|
ServerTargetArgs {
|
|
server: value.map(str::to_string),
|
|
}
|
|
}
|
|
|
|
fn parse_user_settings(source: &str) -> UserSettings {
|
|
UserSettingsBuilder::from_toml(source).expect("fixture should resolve")
|
|
}
|
|
|
|
#[test]
|
|
fn exec_has_no_server_target_by_default() {
|
|
assert_eq!(exec_server_target(&server_target_args(None)).unwrap(), None);
|
|
}
|
|
|
|
#[test]
|
|
fn exec_uses_cli_server_target() {
|
|
assert_eq!(
|
|
exec_server_target(&server_target_args(Some("https://cli.example.com"))).unwrap(),
|
|
Some(ServerTarget::http_url("https://cli.example.com").unwrap())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn exec_supports_explicit_unix_socket_target() {
|
|
assert_eq!(
|
|
exec_server_target(&server_target_args(Some("/tmp/fabro.sock"))).unwrap(),
|
|
Some(ServerTarget::unix_socket_path("/tmp/fabro.sock").unwrap())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn exec_ignores_configured_server_target_without_cli_override() {
|
|
assert_eq!(exec_server_target(&server_target_args(None)).unwrap(), None);
|
|
}
|
|
|
|
#[test]
|
|
fn resolve_server_target_uses_configured_server_target() {
|
|
let settings = parse_user_settings(
|
|
r#"
|
|
_version = 1
|
|
|
|
[cli.target]
|
|
type = "http"
|
|
url = "https://config.example.com"
|
|
"#,
|
|
);
|
|
assert_eq!(
|
|
resolve_server_target(&server_target_args(None), &settings).unwrap(),
|
|
ServerTarget::http_url("https://config.example.com").unwrap()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn resolve_server_target_explicit_target_overrides_config_target() {
|
|
let settings = parse_user_settings(
|
|
r#"
|
|
_version = 1
|
|
|
|
[cli.target]
|
|
type = "http"
|
|
url = "https://config.example.com"
|
|
"#,
|
|
);
|
|
assert_eq!(
|
|
resolve_server_target(
|
|
&server_target_args(Some("https://cli.example.com")),
|
|
&settings
|
|
)
|
|
.unwrap(),
|
|
ServerTarget::http_url("https://cli.example.com").unwrap()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn resolve_server_target_defaults_to_default_unix_socket_target() {
|
|
let settings = UserSettings::default();
|
|
assert_eq!(
|
|
resolve_server_target(&server_target_args(None), &settings).unwrap(),
|
|
ServerTarget::unix_socket_path(dirs::home_dir().unwrap().join(".fabro/fabro.sock"))
|
|
.unwrap()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn explicit_server_target_overrides_config_target() {
|
|
let settings = parse_user_settings(
|
|
r#"
|
|
_version = 1
|
|
|
|
[cli.target]
|
|
type = "http"
|
|
url = "https://config.example.com"
|
|
"#,
|
|
);
|
|
assert_eq!(
|
|
resolve_server_target(
|
|
&server_target_args(Some("https://cli.example.com")),
|
|
&settings
|
|
)
|
|
.unwrap(),
|
|
ServerTarget::http_url("https://cli.example.com").unwrap()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_server_target_is_rejected() {
|
|
let error = exec_server_target(&server_target_args(Some("fabro.internal"))).unwrap_err();
|
|
assert_eq!(
|
|
error.to_string(),
|
|
"server target must be an http(s) URL or absolute Unix socket path"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
#[expect(
|
|
clippy::disallowed_methods,
|
|
reason = "unit test writes a temporary settings fixture with sync std::fs"
|
|
)]
|
|
fn configure_cli_target_creates_settings_file_when_missing() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let config_path = dir.path().join(".fabro").join("settings.toml");
|
|
let settings = UserSettings::default();
|
|
let target = ServerTarget::http_url("http://127.0.0.1:32276").unwrap();
|
|
|
|
assert!(configure_cli_target_if_missing(&config_path, &settings, &target).unwrap());
|
|
|
|
let contents = std::fs::read_to_string(&config_path).unwrap();
|
|
insta::assert_snapshot!(contents, @r#"
|
|
_version = 1
|
|
|
|
[cli.target]
|
|
type = "http"
|
|
url = "http://127.0.0.1:32276"
|
|
"#);
|
|
let settings = UserSettingsBuilder::load_from(&config_path).unwrap();
|
|
assert_eq!(
|
|
resolve_server_target(&server_target_args(None), &settings).unwrap(),
|
|
target
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
#[expect(
|
|
clippy::disallowed_methods,
|
|
reason = "unit test writes a temporary settings fixture with sync std::fs"
|
|
)]
|
|
fn configure_cli_target_does_not_overwrite_existing_target() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let config_path = dir.path().join("settings.toml");
|
|
let existing = r#"
|
|
_version = 1
|
|
|
|
[cli.target]
|
|
type = "http"
|
|
url = "https://configured.example.com"
|
|
"#;
|
|
std::fs::write(&config_path, existing).unwrap();
|
|
let settings = UserSettingsBuilder::load_from(&config_path).unwrap();
|
|
let target = ServerTarget::http_url("https://new.example.com").unwrap();
|
|
|
|
assert!(!configure_cli_target_if_missing(&config_path, &settings, &target).unwrap());
|
|
|
|
assert_eq!(std::fs::read_to_string(&config_path).unwrap(), existing);
|
|
}
|
|
|
|
#[test]
|
|
fn storage_dir_defaults_without_server_auth_methods() {
|
|
let document = toml::Value::Table(toml::Table::new());
|
|
|
|
assert_eq!(
|
|
storage_dir_from_document(&document, None).unwrap(),
|
|
default_storage_dir()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn storage_dir_uses_explicit_server_storage_root() {
|
|
let document: toml::Value = toml::from_str(
|
|
r#"
|
|
_version = 1
|
|
|
|
[server.storage]
|
|
root = "/srv/fabro"
|
|
"#,
|
|
)
|
|
.expect("fixture should parse");
|
|
|
|
assert_eq!(
|
|
storage_dir_from_document(&document, None).unwrap(),
|
|
PathBuf::from("/srv/fabro")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn storage_dir_resolves_env_interpolated_root() {
|
|
let document: toml::Value = toml::from_str(
|
|
r#"
|
|
_version = 1
|
|
|
|
[server.storage]
|
|
root = "{{ env.FABRO_STORAGE_ROOT }}"
|
|
"#,
|
|
)
|
|
.expect("fixture should parse");
|
|
let temp = tempfile::tempdir().unwrap();
|
|
|
|
assert_eq!(
|
|
storage_dir_from_document_with_lookup(&document, None, &|name| {
|
|
(name == "FABRO_STORAGE_ROOT").then(|| temp.path().display().to_string())
|
|
})
|
|
.unwrap(),
|
|
temp.path()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
#[expect(
|
|
clippy::disallowed_methods,
|
|
reason = "unit test writes a temporary settings fixture with sync std::fs::write"
|
|
)]
|
|
fn load_settings_document_uses_fabro_config_env_for_storage_root() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let config_path = dir.path().join("settings.toml");
|
|
std::fs::write(
|
|
&config_path,
|
|
r#"
|
|
_version = 1
|
|
|
|
[server.storage]
|
|
root = "/srv/fabro"
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let document = load_settings_document_with_lookup(None, |_| {
|
|
Some(config_path.clone().into_os_string())
|
|
})
|
|
.expect("settings document should load");
|
|
|
|
assert_eq!(
|
|
storage_dir_from_document(&document, None).unwrap(),
|
|
PathBuf::from("/srv/fabro")
|
|
);
|
|
}
|
|
}
|