use std::sync::Mutex;
use std::sync::atomic::{AtomicUsize, Ordering};
use serial_test::serial;
use tracing::field::{Field, Visit};
use tracing_subscriber::layer::{Context, Layer};
use tracing_subscriber::prelude::*;
use trusty_common::credentials::KeyStoreError;
use super::*;
#[derive(Default)]
struct Sink {
lines: Mutex<Vec<String>>,
}
struct CaptureLayer {
sink: Arc<Sink>,
}
struct Fields {
out: String,
}
impl Visit for Fields {
fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
use std::fmt::Write as _;
let _ = write!(self.out, " {}={value:?}", field.name());
}
fn record_str(&mut self, field: &Field, value: &str) {
use std::fmt::Write as _;
let _ = write!(self.out, " {}={value}", field.name());
}
}
impl<S: tracing::Subscriber> Layer<S> for CaptureLayer {
fn on_event(&self, event: &tracing::Event<'_>, _ctx: Context<'_, S>) {
let mut fields = Fields {
out: event.metadata().level().to_string(),
};
event.record(&mut fields);
self.sink
.lines
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(fields.out);
}
}
fn capture<T>(f: impl FnOnce() -> T) -> (T, Vec<String>) {
let sink = Arc::new(Sink::default());
let subscriber = tracing_subscriber::registry().with(CaptureLayer {
sink: Arc::clone(&sink),
});
let out = tracing::subscriber::with_default(subscriber, f);
let lines = sink
.lines
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
(out, lines)
}
fn assert_logged(lines: &[String], var: &str, kind: &str) {
let errors: Vec<&String> = lines.iter().filter(|l| l.starts_with("ERROR")).collect();
assert_eq!(errors.len(), 1, "expected one ERROR line, got {lines:?}");
assert!(errors[0].contains(var), "{}", errors[0]);
assert!(
errors[0].contains(&format!("kind={kind}")),
"expected kind={kind}: {}",
errors[0]
);
}
struct EnvVarGuard {
key: &'static str,
prev: Option<String>,
}
impl EnvVarGuard {
fn unset(key: &'static str) -> Self {
let prev = std::env::var(key).ok();
unsafe {
std::env::remove_var(key);
}
Self { key, prev }
}
fn set(key: &'static str, value: &str) -> Self {
let prev = std::env::var(key).ok();
unsafe {
std::env::set_var(key, value);
}
Self { key, prev }
}
}
impl Drop for EnvVarGuard {
fn drop(&mut self) {
unsafe {
match &self.prev {
Some(v) => std::env::set_var(self.key, v),
None => std::env::remove_var(self.key),
}
}
}
}
struct AbsentStore {
calls: Arc<AtomicUsize>,
}
impl KeyStore for AbsentStore {
fn get(&self, _provider: &str) -> Option<String> {
self.calls.fetch_add(1, Ordering::SeqCst);
None
}
fn set(&self, _provider: &str, _value: &str) -> Result<(), KeyStoreError> {
Ok(())
}
fn unset(&self, _provider: &str) -> Result<(), KeyStoreError> {
Ok(())
}
fn list(&self) -> Vec<String> {
Vec::new()
}
}
struct NeverReturns;
impl KeyStore for NeverReturns {
fn get(&self, _provider: &str) -> Option<String> {
loop {
std::thread::sleep(Duration::from_secs(3600));
}
}
fn set(&self, _provider: &str, _value: &str) -> Result<(), KeyStoreError> {
Ok(())
}
fn unset(&self, _provider: &str) -> Result<(), KeyStoreError> {
Ok(())
}
fn list(&self) -> Vec<String> {
Vec::new()
}
}
struct RefusingStore;
impl KeyStore for RefusingStore {
fn get(&self, _provider: &str) -> Option<String> {
None
}
fn try_get(&self, _provider: &str) -> Result<Option<String>, KeyStoreError> {
Err(KeyStoreError::Keyring("locked".to_string()))
}
fn set(&self, _provider: &str, _value: &str) -> Result<(), KeyStoreError> {
Ok(())
}
fn unset(&self, _provider: &str) -> Result<(), KeyStoreError> {
Ok(())
}
fn list(&self) -> Vec<String> {
Vec::new()
}
}
#[test]
#[serial]
fn an_absent_secret_is_none_and_logs_absent() {
let _guard = EnvVarGuard::unset("BITBUCKET_APP_PASSWORD");
let calls = Arc::new(AtomicUsize::new(0));
let store = Arc::new(AbsentStore {
calls: Arc::clone(&calls),
});
let (resolved, lines) = capture(|| {
resolve_secret_with("BITBUCKET_APP_PASSWORD", store, Duration::from_millis(500))
});
assert!(resolved.is_none(), "an absent credential must not resolve");
assert_eq!(
calls.load(Ordering::SeqCst),
1,
"the store was consulted once"
);
assert_logged(&lines, "BITBUCKET_APP_PASSWORD", "absent");
}
#[test]
#[serial]
fn a_store_timeout_is_none_and_logs_timeout() {
let _guard = EnvVarGuard::unset("JIRA_API_TOKEN");
let bound = Duration::from_millis(200);
let started = std::time::Instant::now();
let (resolved, lines) =
capture(|| resolve_secret_with("JIRA_API_TOKEN", Arc::new(NeverReturns), bound));
let waited = started.elapsed();
assert!(resolved.is_none(), "a timed-out read must not resolve");
assert!(
waited < Duration::from_secs(2),
"the caller waited {waited:?}"
);
assert_logged(&lines, "JIRA_API_TOKEN", "timeout");
}
#[test]
#[serial]
fn a_store_error_is_none_and_logs_the_kind() {
let _guard = EnvVarGuard::unset("LINEAR_API_KEY");
let (resolved, lines) = capture(|| {
resolve_secret_with(
"LINEAR_API_KEY",
Arc::new(RefusingStore),
Duration::from_millis(500),
)
});
assert!(resolved.is_none(), "a refused store must not resolve");
assert_logged(&lines, "LINEAR_API_KEY", "keyring-backend");
}
#[test]
#[serial]
fn an_unregistered_variable_reads_only_the_process_environment() {
let calls = Arc::new(AtomicUsize::new(0));
let store = || {
Arc::new(AbsentStore {
calls: Arc::clone(&calls),
})
};
let present = EnvVarGuard::set("TM_TEST_UNREGISTERED_8236", "from-the-process-env");
let (resolved, lines) = capture(|| {
resolve_secret_with(
"TM_TEST_UNREGISTERED_8236",
store(),
Duration::from_millis(500),
)
});
assert_eq!(resolved.as_deref(), Some("from-the-process-env"));
assert!(lines.is_empty(), "a success must log nothing: {lines:?}");
drop(present);
let (resolved, lines) = capture(|| {
resolve_secret_with(
"TM_TEST_UNREGISTERED_8236",
store(),
Duration::from_millis(500),
)
});
assert!(resolved.is_none(), "an unset variable must not resolve");
assert_logged(&lines, "TM_TEST_UNREGISTERED_8236", "absent");
assert_eq!(
calls.load(Ordering::SeqCst),
0,
"an unregistered name must never reach the credential store"
);
}