use crate::secret::provider::ProbeResult;
use crate::secret::registry::SecretRegistry;
use crate::DodotError;
pub fn render_probe_outcome(scheme: &str, outcome: &ProbeResult) -> String {
let config_key = crate::secret::registry::scheme_to_config_key(scheme);
match outcome {
ProbeResult::Ok => String::new(),
ProbeResult::NotInstalled { hint } => format!(
"secret provider `{scheme}` is not installed\n \
{hint}\n \
or disable the provider: [secret.providers.{config_key}] enabled = false"
),
ProbeResult::NotAuthenticated { hint } => format!(
"secret provider `{scheme}` is not authenticated\n \
{hint}"
),
ProbeResult::Misconfigured { hint } => {
format!("secret provider `{scheme}` is misconfigured\n {hint}")
}
ProbeResult::ProbeFailed { details } => format!(
"secret provider `{scheme}` probe failed\n \
{details}\n \
this is unusual; check the dodot debug log (`dodot --debug ...`) for more"
),
}
}
pub fn preflight(registry: &SecretRegistry) -> crate::Result<()> {
let outcomes = registry.probe_all();
let mut failing: Vec<String> = Vec::new();
for (scheme, outcome) in &outcomes {
if !outcome.is_ok() {
failing.push(render_probe_outcome(scheme, outcome));
}
}
if failing.is_empty() {
return Ok(());
}
Err(DodotError::Other(format!(
"{} secret provider(s) need attention before `dodot up` can resolve secrets:\n\n{}",
failing.len(),
failing.join("\n\n")
)))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::secret::test_support::MockSecretProvider;
use std::sync::Arc;
#[test]
fn render_ok_returns_empty_string() {
assert_eq!(render_probe_outcome("op", &ProbeResult::Ok), "");
}
#[test]
fn render_not_installed_includes_install_hint_and_disable_pointer() {
let outcome = ProbeResult::NotInstalled {
hint: "install 1Password CLI: https://1password.com/downloads/command-line".into(),
};
let msg = render_probe_outcome("op", &outcome);
assert!(msg.contains("`op` is not installed"));
assert!(msg.contains("1password.com"));
assert!(msg.contains("[secret.providers.op] enabled = false"));
}
#[test]
fn render_not_installed_uses_underscore_key_for_secret_tool() {
let outcome = ProbeResult::NotInstalled {
hint: "install secret-tool".into(),
};
let msg = render_probe_outcome("secret-tool", &outcome);
assert!(msg.contains("`secret-tool` is not installed"));
assert!(
msg.contains("[secret.providers.secret_tool] enabled = false"),
"expected underscore TOML key in disable hint, got: {msg}"
);
assert!(
!msg.contains("[secret.providers.secret-tool]"),
"hyphen form must not leak: {msg}"
);
}
#[test]
fn render_not_authenticated_surfaces_provider_hint() {
let outcome = ProbeResult::NotAuthenticated {
hint: "set OP_SERVICE_ACCOUNT_TOKEN".into(),
};
let msg = render_probe_outcome("op", &outcome);
assert!(msg.contains("`op` is not authenticated"));
assert!(msg.contains("OP_SERVICE_ACCOUNT_TOKEN"));
}
#[test]
fn render_misconfigured_surfaces_provider_hint_verbatim() {
let outcome = ProbeResult::Misconfigured {
hint: "password store not initialised at /tmp/store".into(),
};
let msg = render_probe_outcome("pass", &outcome);
assert!(msg.contains("`pass` is misconfigured"));
assert!(msg.contains("/tmp/store"));
}
#[test]
fn render_probe_failed_includes_debug_pointer() {
let outcome = ProbeResult::ProbeFailed {
details: "subprocess crashed mid-probe".into(),
};
let msg = render_probe_outcome("op", &outcome);
assert!(msg.contains("probe failed"));
assert!(msg.contains("subprocess crashed"));
assert!(msg.contains("`dodot --debug"));
}
#[test]
fn preflight_succeeds_when_all_providers_ok() {
let mut reg = SecretRegistry::new();
reg.register(Arc::new(MockSecretProvider::new("pass")));
reg.register(Arc::new(MockSecretProvider::new("op")));
assert!(preflight(®).is_ok());
}
#[test]
fn preflight_fails_with_aggregated_message_when_one_provider_fails() {
let mut reg = SecretRegistry::new();
reg.register(Arc::new(MockSecretProvider::new("pass"))); reg.register(Arc::new(MockSecretProvider::new("op").with_probe(
ProbeResult::NotAuthenticated {
hint: "set OP_SERVICE_ACCOUNT_TOKEN".into(),
},
)));
let err = preflight(®).unwrap_err().to_string();
assert!(err.contains("1 secret provider(s) need attention"));
assert!(!err.contains("`pass`"));
assert!(err.contains("`op` is not authenticated"));
assert!(err.contains("OP_SERVICE_ACCOUNT_TOKEN"));
}
#[test]
fn preflight_aggregates_multiple_failures_into_one_message() {
let mut reg = SecretRegistry::new();
reg.register(Arc::new(MockSecretProvider::new("op").with_probe(
ProbeResult::NotAuthenticated {
hint: "set OP_SERVICE_ACCOUNT_TOKEN".into(),
},
)));
reg.register(Arc::new(MockSecretProvider::new("pass").with_probe(
ProbeResult::Misconfigured {
hint: "store not initialised".into(),
},
)));
let err = preflight(®).unwrap_err().to_string();
assert!(err.contains("2 secret provider(s) need attention"));
assert!(err.contains("`op` is not authenticated"));
assert!(err.contains("`pass` is misconfigured"));
}
#[test]
fn preflight_succeeds_on_empty_registry() {
let reg = SecretRegistry::new();
assert!(preflight(®).is_ok());
}
}