use std::collections::HashMap;
use std::convert::TryFrom;
use std::sync::Arc;
use std::sync::Mutex;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
use std::time::Duration;
#[cfg(test)]
use tempfile::TempDir;
use crate::Result;
use crate::SecretBytes;
use crate::provider::Address;
use crate::provider::DiscoveryContext;
use crate::provider::Provider;
pub struct MockProvider {
storage: Arc<Mutex<HashMap<String, String>>>,
}
impl MockProvider {
pub fn new() -> Self {
Self {
storage: Arc::new(Mutex::new(HashMap::new())),
}
}
}
impl Provider for MockProvider {
fn convention_address(
&self,
project: &str,
profile: &str,
key: &str,
) -> Result<crate::config::NativeAddress> {
Ok(crate::config::NativeAddress {
item: format!("{project}/{profile}/{key}"),
..Default::default()
})
}
fn get(&self, addr: Address<'_>) -> Result<Option<SecretBytes>> {
let full_key = super::flat_item(self, addr)?;
let storage = self.storage.lock().unwrap();
Ok(storage
.get(&*full_key)
.map(|v| SecretBytes::from_utf8(v.clone())))
}
fn set(&self, addr: Address<'_>, value: &SecretBytes) -> Result<()> {
let full_key = super::flat_item(self, addr)?.into_owned();
let mut storage = self.storage.lock().unwrap();
storage.insert(full_key, value.try_as_utf8().unwrap().to_string());
Ok(())
}
fn delete(&self, addr: Address<'_>) -> Result<bool> {
let item = super::flat_item(self, addr)?.into_owned();
Ok(self.storage.lock().unwrap().remove(&item).is_some())
}
fn name(&self) -> &'static str {
"mock"
}
fn uri(&self) -> String {
"mock://".to_string()
}
}
struct CountingProvider {
values: HashMap<String, String>,
gets: Arc<Mutex<HashMap<String, usize>>>,
}
impl CountingProvider {
fn new(values: &[(&str, &str)]) -> Self {
Self {
values: values
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
gets: Arc::new(Mutex::new(HashMap::new())),
}
}
fn get_count(&self, item: &str) -> usize {
self.gets.lock().unwrap().get(item).copied().unwrap_or(0)
}
}
impl Provider for CountingProvider {
fn convention_address(
&self,
project: &str,
profile: &str,
key: &str,
) -> Result<crate::config::NativeAddress> {
Ok(crate::config::NativeAddress {
item: format!("{project}/{profile}/{key}"),
..Default::default()
})
}
fn get(&self, addr: Address<'_>) -> Result<Option<SecretBytes>> {
let item = super::flat_item(self, addr)?.into_owned();
*self.gets.lock().unwrap().entry(item.clone()).or_insert(0) += 1;
Ok(self
.values
.get(&item)
.map(|v| SecretBytes::from_utf8(v.clone())))
}
fn set(&self, _addr: Address<'_>, _value: &SecretBytes) -> Result<()> {
Ok(())
}
fn name(&self) -> &'static str {
"counting"
}
fn uri(&self) -> String {
"counting://".to_string()
}
}
static MEM_STORE: std::sync::LazyLock<Mutex<HashMap<String, String>>> =
std::sync::LazyLock::new(|| Mutex::new(HashMap::new()));
pub(crate) struct MemTestProvider;
pub(crate) struct MemTestConfig;
impl TryFrom<&super::ProviderUrl> for MemTestConfig {
type Error = crate::MonosecretError;
fn try_from(_url: &super::ProviderUrl) -> Result<Self> {
Ok(Self)
}
}
impl MemTestProvider {
fn new(_config: MemTestConfig) -> Self {
Self
}
}
crate::register_provider! {
struct: MemTestProvider,
config: MemTestConfig,
name: "memtest",
description: "In-memory provider for tests",
schemes: ["memtest"],
examples: ["memtest://"],
credential_names: ["test_token"],
deletes: true,
}
impl Provider for MemTestProvider {
fn convention_address(
&self,
project: &str,
profile: &str,
key: &str,
) -> Result<crate::config::NativeAddress> {
Ok(crate::config::NativeAddress {
item: format!("{project}/{profile}/{key}"),
..Default::default()
})
}
fn get(&self, addr: Address<'_>) -> Result<Option<SecretBytes>> {
let item = super::flat_item(self, addr)?.into_owned();
Ok(MEM_STORE
.lock()
.unwrap()
.get(&item)
.map(|v| SecretBytes::from_utf8(v.clone())))
}
fn set(&self, addr: Address<'_>, value: &SecretBytes) -> Result<()> {
let item = super::flat_item(self, addr)?.into_owned();
MEM_STORE
.lock()
.unwrap()
.insert(item, value.try_as_utf8().unwrap().to_string());
Ok(())
}
fn delete(&self, addr: Address<'_>) -> Result<bool> {
let item = super::flat_item(self, addr)?.into_owned();
Ok(MEM_STORE.lock().unwrap().remove(&item).is_some())
}
fn name(&self) -> &str {
Self::PROVIDER_NAME
}
fn uri(&self) -> String {
"memtest://".to_string()
}
}
static SLOW_CURRENT: AtomicUsize = AtomicUsize::new(0);
static SLOW_PEAK: AtomicUsize = AtomicUsize::new(0);
pub(crate) fn reset_slow_peak() {
SLOW_CURRENT.store(0, Ordering::SeqCst);
SLOW_PEAK.store(0, Ordering::SeqCst);
}
pub(crate) fn slow_peak() -> usize {
SLOW_PEAK.load(Ordering::SeqCst)
}
pub(crate) struct SlowTestProvider;
pub(crate) struct SlowTestConfig;
impl TryFrom<&super::ProviderUrl> for SlowTestConfig {
type Error = crate::MonosecretError;
fn try_from(_url: &super::ProviderUrl) -> Result<Self> {
Ok(Self)
}
}
impl SlowTestProvider {
fn new(_config: SlowTestConfig) -> Self {
Self
}
}
crate::register_provider! {
struct: SlowTestProvider,
config: SlowTestConfig,
name: "slowtest",
description: "Slow in-memory provider for fallback concurrency tests",
schemes: ["slowtest"],
examples: ["slowtest://"],
deletes: true,
}
impl Provider for SlowTestProvider {
fn convention_address(
&self,
project: &str,
profile: &str,
key: &str,
) -> Result<crate::config::NativeAddress> {
MemTestProvider.convention_address(project, profile, key)
}
fn get(&self, addr: Address<'_>) -> Result<Option<SecretBytes>> {
let item = super::flat_item(self, addr)?.into_owned();
let current = SLOW_CURRENT.fetch_add(1, Ordering::SeqCst) + 1;
SLOW_PEAK.fetch_max(current, Ordering::SeqCst);
std::thread::sleep(Duration::from_millis(50));
SLOW_CURRENT.fetch_sub(1, Ordering::SeqCst);
Ok(MEM_STORE
.lock()
.unwrap()
.get(&item)
.map(|value| SecretBytes::from_utf8(value.clone())))
}
fn set(&self, addr: Address<'_>, value: &SecretBytes) -> Result<()> {
MemTestProvider.set(addr, value)
}
fn delete(&self, addr: Address<'_>) -> Result<bool> {
MemTestProvider.delete(addr)
}
fn name(&self) -> &str {
Self::PROVIDER_NAME
}
fn uri(&self) -> String {
"slowtest://".to_string()
}
}
pub(crate) struct StatefulTestProvider {
snapshot: std::sync::OnceLock<HashMap<String, String>>,
reason: Mutex<Option<String>>,
requested_authorization_duration: Mutex<Option<Duration>>,
caller: Mutex<Option<crate::CallerContext>>,
}
pub(crate) struct StatefulTestConfig;
static STATEFUL_REASON_READS: std::sync::LazyLock<Mutex<HashMap<String, Vec<Option<String>>>>> =
std::sync::LazyLock::new(|| Mutex::new(HashMap::new()));
static STATEFUL_CALLER_READS: std::sync::LazyLock<
Mutex<HashMap<String, Vec<Option<crate::CallerContext>>>>,
> = std::sync::LazyLock::new(|| Mutex::new(HashMap::new()));
static STATEFUL_AUTHORIZATION_DURATION_READS: std::sync::LazyLock<
Mutex<HashMap<String, Vec<Option<Duration>>>>,
> = std::sync::LazyLock::new(|| Mutex::new(HashMap::new()));
impl TryFrom<&super::ProviderUrl> for StatefulTestConfig {
type Error = crate::MonosecretError;
fn try_from(_url: &super::ProviderUrl) -> Result<Self> {
Ok(Self)
}
}
impl StatefulTestProvider {
fn new(_config: StatefulTestConfig) -> Self {
Self {
snapshot: std::sync::OnceLock::new(),
reason: Mutex::new(None),
requested_authorization_duration: Mutex::new(None),
caller: Mutex::new(None),
}
}
}
crate::register_provider! {
struct: StatefulTestProvider,
config: StatefulTestConfig,
name: "statefultest",
description: "Stateful in-memory provider for provider-lifetime tests",
schemes: ["statefultest"],
examples: ["statefultest://"],
deletes: true,
}
impl Provider for StatefulTestProvider {
fn convention_address(
&self,
project: &str,
profile: &str,
key: &str,
) -> Result<crate::config::NativeAddress> {
MemTestProvider.convention_address(project, profile, key)
}
fn get(&self, addr: Address<'_>) -> Result<Option<SecretBytes>> {
let item = super::flat_item(self, addr)?.into_owned();
STATEFUL_REASON_READS
.lock()
.unwrap()
.entry(item.clone())
.or_default()
.push(self.reason.lock().unwrap().clone());
STATEFUL_CALLER_READS
.lock()
.unwrap()
.entry(item.clone())
.or_default()
.push(self.caller.lock().unwrap().clone());
STATEFUL_AUTHORIZATION_DURATION_READS
.lock()
.unwrap()
.entry(item.clone())
.or_default()
.push(*self.requested_authorization_duration.lock().unwrap());
let snapshot = self
.snapshot
.get_or_init(|| MEM_STORE.lock().unwrap().clone());
Ok(snapshot
.get(&item)
.map(|value| SecretBytes::from_utf8(value.clone())))
}
fn set(&self, addr: Address<'_>, value: &SecretBytes) -> Result<()> {
MemTestProvider.set(addr, value)
}
fn delete(&self, addr: Address<'_>) -> Result<bool> {
MemTestProvider.delete(addr)
}
fn name(&self) -> &str {
Self::PROVIDER_NAME
}
fn uri(&self) -> String {
"statefultest://".to_string()
}
fn set_reason(&self, reason: Option<String>) {
*self.reason.lock().unwrap() = reason;
}
fn set_requested_authorization_duration(&self, duration: Option<Duration>) {
*self.requested_authorization_duration.lock().unwrap() = duration;
}
fn set_caller(&self, caller: Option<crate::CallerContext>) {
*self.caller.lock().unwrap() = caller;
}
}
pub(crate) fn take_stateful_reason_reads(item: &str) -> Vec<Option<String>> {
STATEFUL_REASON_READS
.lock()
.unwrap()
.remove(item)
.unwrap_or_default()
}
pub(crate) fn take_stateful_caller_reads(item: &str) -> Vec<Option<crate::CallerContext>> {
STATEFUL_CALLER_READS
.lock()
.unwrap()
.remove(item)
.unwrap_or_default()
}
pub(crate) fn take_stateful_authorization_duration_reads(item: &str) -> Vec<Option<Duration>> {
STATEFUL_AUTHORIZATION_DURATION_READS
.lock()
.unwrap()
.remove(item)
.unwrap_or_default()
}
pub(crate) struct FailWriteProvider;
pub(crate) struct FailWriteConfig;
impl TryFrom<&super::ProviderUrl> for FailWriteConfig {
type Error = crate::MonosecretError;
fn try_from(_url: &super::ProviderUrl) -> Result<Self> {
Ok(Self)
}
}
impl FailWriteProvider {
fn new(_config: FailWriteConfig) -> Self {
Self
}
}
crate::register_provider! {
struct: FailWriteProvider,
config: FailWriteConfig,
name: "failwrite",
description: "In-memory provider for tests whose writes always fail",
schemes: ["failwrite"],
examples: ["failwrite://"],
deletes: true,
}
impl Provider for FailWriteProvider {
fn convention_address(
&self,
project: &str,
profile: &str,
key: &str,
) -> Result<crate::config::NativeAddress> {
Ok(crate::config::NativeAddress {
item: format!("{project}/{profile}/{key}"),
..Default::default()
})
}
fn get(&self, addr: Address<'_>) -> Result<Option<SecretBytes>> {
MemTestProvider.get(addr)
}
fn set(&self, _addr: Address<'_>, _value: &SecretBytes) -> Result<()> {
Err(crate::MonosecretError::ProviderOperationFailed(
"failwrite always fails to write".to_string(),
))
}
fn delete(&self, addr: Address<'_>) -> Result<bool> {
MemTestProvider.delete(addr)
}
fn name(&self) -> &str {
Self::PROVIDER_NAME
}
fn uri(&self) -> String {
"failwrite://".to_string()
}
}
pub(crate) struct FailDeleteProvider;
pub(crate) struct FailDeleteConfig;
impl TryFrom<&super::ProviderUrl> for FailDeleteConfig {
type Error = crate::MonosecretError;
fn try_from(_url: &super::ProviderUrl) -> Result<Self> {
Ok(Self)
}
}
impl FailDeleteProvider {
fn new(_config: FailDeleteConfig) -> Self {
Self
}
}
crate::register_provider! {
struct: FailDeleteProvider,
config: FailDeleteConfig,
name: "faildelete",
description: "In-memory provider for tests whose deletes always fail",
schemes: ["faildelete"],
examples: ["faildelete://"],
deletes: true,
}
impl Provider for FailDeleteProvider {
fn convention_address(
&self,
project: &str,
profile: &str,
key: &str,
) -> Result<crate::config::NativeAddress> {
MemTestProvider.convention_address(project, profile, key)
}
fn get(&self, addr: Address<'_>) -> Result<Option<SecretBytes>> {
MemTestProvider.get(addr)
}
fn set(&self, addr: Address<'_>, value: &SecretBytes) -> Result<()> {
MemTestProvider.set(addr, value)
}
fn delete(&self, _addr: Address<'_>) -> Result<bool> {
Err(crate::MonosecretError::ProviderOperationFailed(
"faildelete always fails to delete".to_string(),
))
}
fn name(&self) -> &str {
Self::PROVIDER_NAME
}
fn uri(&self) -> String {
"faildelete://".to_string()
}
}
static EXPIRING_TTLS: std::sync::LazyLock<Mutex<HashMap<String, Duration>>> =
std::sync::LazyLock::new(|| Mutex::new(HashMap::new()));
pub(crate) fn recorded_expiry(item: &str) -> Option<Duration> {
EXPIRING_TTLS.lock().unwrap().get(item).copied()
}
pub(crate) struct ExpiringProvider;
pub(crate) struct ExpiringConfig;
impl TryFrom<&super::ProviderUrl> for ExpiringConfig {
type Error = crate::MonosecretError;
fn try_from(_url: &super::ProviderUrl) -> Result<Self> {
Ok(Self)
}
}
impl ExpiringProvider {
fn new(_config: ExpiringConfig) -> Self {
Self
}
}
crate::register_provider! {
struct: ExpiringProvider,
config: ExpiringConfig,
name: "expiring",
description: "In-memory provider for tests that records requested expiries",
schemes: ["expiring"],
examples: ["expiring://"],
deletes: true,
}
impl Provider for ExpiringProvider {
fn convention_address(
&self,
project: &str,
profile: &str,
key: &str,
) -> Result<crate::config::NativeAddress> {
MemTestProvider.convention_address(project, profile, key)
}
fn get(&self, addr: Address<'_>) -> Result<Option<SecretBytes>> {
MemTestProvider.get(addr)
}
fn set(&self, addr: Address<'_>, value: &SecretBytes) -> Result<()> {
MemTestProvider.set(addr, value)
}
fn set_expiring(
&self,
addr: Address<'_>,
value: &SecretBytes,
max_age: Duration,
) -> Result<()> {
let item = super::flat_item(self, addr)?.into_owned();
EXPIRING_TTLS.lock().unwrap().insert(item, max_age);
self.set(addr, value)
}
fn delete(&self, addr: Address<'_>) -> Result<bool> {
MemTestProvider.delete(addr)
}
fn name(&self) -> &str {
Self::PROVIDER_NAME
}
fn uri(&self) -> String {
"expiring://".to_string()
}
}
#[test]
#[allow(clippy::indexing_slicing)] fn get_each_dedupes_one_address_across_names() {
let p = CountingProvider::new(&[("svc", "val")]);
let coords = crate::config::NativeAddress {
item: "svc".into(),
..Default::default()
};
let addr = Address::Native(&coords);
let out = super::get_each(&p, &[("FIRST", addr), ("SECOND", addr)]).unwrap();
assert_eq!(out["FIRST"].expose_secret(), b"val");
assert_eq!(out["SECOND"].expose_secret(), b"val");
assert_eq!(p.get_count("svc"), 1, "one address must be fetched once");
}
#[test]
#[allow(clippy::indexing_slicing)] fn get_each_fetches_distinct_addresses_and_omits_missing() {
let p = CountingProvider::new(&[("one", "v1"), ("two", "v2")]);
let a1 = crate::config::NativeAddress {
item: "one".into(),
..Default::default()
};
let a2 = crate::config::NativeAddress {
item: "two".into(),
..Default::default()
};
let a3 = crate::config::NativeAddress {
item: "absent".into(),
..Default::default()
};
let out = super::get_each(
&p,
&[
("A", Address::Native(&a1)),
("B", Address::Native(&a2)),
("C", Address::Native(&a3)),
],
)
.unwrap();
assert_eq!(out["A"].expose_secret(), b"v1");
assert_eq!(out["B"].expose_secret(), b"v2");
assert!(!out.contains_key("C"), "a missing secret is omitted");
assert_eq!(p.get_count("one"), 1);
assert_eq!(p.get_count("two"), 1);
assert_eq!(p.get_count("absent"), 1);
}
struct PeakConcurrencyProvider {
delay: Duration,
current: AtomicUsize,
peak: AtomicUsize,
}
impl PeakConcurrencyProvider {
fn new(delay: Duration) -> Self {
Self {
delay,
current: AtomicUsize::new(0),
peak: AtomicUsize::new(0),
}
}
fn peak(&self) -> usize {
self.peak.load(Ordering::SeqCst)
}
}
impl Provider for PeakConcurrencyProvider {
fn convention_address(
&self,
project: &str,
profile: &str,
key: &str,
) -> Result<crate::config::NativeAddress> {
Ok(crate::config::NativeAddress {
item: format!("{project}/{profile}/{key}"),
..Default::default()
})
}
fn get(&self, addr: Address<'_>) -> Result<Option<SecretBytes>> {
let item = super::flat_item(self, addr)?.into_owned();
let now = self.current.fetch_add(1, Ordering::SeqCst) + 1;
let mut peak = self.peak.load(Ordering::SeqCst);
while now > peak {
match self
.peak
.compare_exchange(peak, now, Ordering::SeqCst, Ordering::SeqCst)
{
Ok(_) => break,
Err(observed) => peak = observed,
}
}
std::thread::sleep(self.delay);
self.current.fetch_sub(1, Ordering::SeqCst);
Ok(Some(SecretBytes::from_utf8(item)))
}
fn set(&self, _addr: Address<'_>, _value: &SecretBytes) -> Result<()> {
Ok(())
}
fn name(&self) -> &'static str {
"peak"
}
fn uri(&self) -> String {
"peak://".to_string()
}
}
#[test]
fn get_each_concurrency_defaults_and_parses_env() {
let _lock = crate::tests::scrub_resolution_env();
let _clear = crate::tests::EnvVarGuard::remove(super::GET_EACH_CONCURRENCY_ENV);
assert_eq!(super::get_each_concurrency(), 8);
let set = crate::tests::EnvVarGuard::set(super::GET_EACH_CONCURRENCY_ENV, "4");
assert_eq!(super::get_each_concurrency(), 4);
drop(set);
let zero = crate::tests::EnvVarGuard::set(super::GET_EACH_CONCURRENCY_ENV, "0");
assert_eq!(
super::get_each_concurrency(),
8,
"zero is invalid and must fall back"
);
drop(zero);
let _bad = crate::tests::EnvVarGuard::set(super::GET_EACH_CONCURRENCY_ENV, "nope");
assert_eq!(super::get_each_concurrency(), 8);
}
#[test]
#[allow(clippy::indexing_slicing)] fn get_each_respects_concurrency_cap() {
let _lock = crate::tests::scrub_resolution_env();
let _env = crate::tests::EnvVarGuard::set(super::GET_EACH_CONCURRENCY_ENV, "3");
let p = PeakConcurrencyProvider::new(Duration::from_millis(80));
let coords: Vec<crate::config::NativeAddress> = (0..10)
.map(|i| {
crate::config::NativeAddress {
item: format!("item-{i}"),
..Default::default()
}
})
.collect();
let requests: Vec<(&str, Address<'_>)> = coords
.iter()
.enumerate()
.map(|(i, c)| {
(
["n0", "n1", "n2", "n3", "n4", "n5", "n6", "n7", "n8", "n9"][i],
Address::Native(c),
)
})
.collect();
let out = super::get_each(&p, &requests).unwrap();
assert_eq!(out.len(), 10);
assert!(
p.peak() <= 3,
"peak in-flight gets {} exceeded concurrency cap 3",
p.peak()
);
assert!(
p.peak() >= 2,
"expected some concurrency, peak={}",
p.peak()
);
}
#[test]
fn test_create_from_string_with_full_uris() {
let provider = Box::<dyn Provider>::try_from("onepassword://Private").unwrap();
assert_eq!(provider.name(), "onepassword");
let provider = Box::<dyn Provider>::try_from("onepassword://work@Production").unwrap();
assert_eq!(provider.name(), "onepassword");
let provider = Box::<dyn Provider>::try_from("onepassword+token://Private").unwrap();
assert_eq!(provider.name(), "onepassword");
}
#[test]
fn every_scheme_rejects_a_userinfo_password() {
const SECRET: &str = "leaked_pw_DO_NOT_ECHO";
for reg in super::PROVIDER_REGISTRY {
for &scheme in reg.metadata.schemes {
let source = format!("{scheme}://attribution:{SECRET}@host/path");
let Err(error) = Box::<dyn Provider>::try_from(source.as_str()) else {
panic!("provider scheme {scheme:?} accepted a URL password");
};
let message = error.to_string();
assert!(
!message.contains(SECRET),
"provider scheme {scheme:?} echoed a URL password into its error: {message:?}"
);
for credential in reg.metadata.credential_names {
assert!(
message.contains(credential),
"provider scheme {scheme:?} refused a URL password without naming its \
{credential:?} credential: {message:?}"
);
}
}
}
}
#[test]
fn set_profile_preserves_provider_identities_across_the_registry() {
for reg in super::PROVIDER_REGISTRY {
assert!(
!reg.metadata.info.examples.is_empty(),
"provider {:?} has no registered example for the set_profile identity invariant",
reg.metadata.info.name
);
for &example in reg.metadata.info.examples {
let provider = Box::<dyn Provider>::try_from(example).unwrap_or_else(|error| {
panic!(
"provider {:?} registered an example that could not be built ({example:?}): \
{error}",
reg.metadata.info.name
)
});
let uri = provider.uri();
let storage_identity = provider.storage_identity();
let entry_container_identity = provider.entry_container_identity();
provider.set_profile("set-profile-identity-invariant");
assert_eq!(
provider.uri(),
uri,
"provider {:?} changed uri() after set_profile (example {example:?})",
reg.metadata.info.name
);
assert_eq!(
provider.storage_identity(),
storage_identity,
"provider {:?} changed storage_identity() after set_profile (example \
{example:?})",
reg.metadata.info.name
);
assert_eq!(
provider.entry_container_identity(),
entry_container_identity,
"provider {:?} changed entry_container_identity() after set_profile (example \
{example:?})",
reg.metadata.info.name
);
}
}
}
#[test]
fn test_create_from_string_with_plain_names() {
let provider = Box::<dyn Provider>::try_from("env").unwrap();
assert_eq!(provider.name(), "env");
let provider = Box::<dyn Provider>::try_from("systemd-credential").unwrap();
assert_eq!(provider.name(), "systemd-credential");
let provider = Box::<dyn Provider>::try_from("keyring").unwrap();
assert_eq!(provider.name(), "keyring");
let provider = Box::<dyn Provider>::try_from("dotenv").unwrap();
assert_eq!(provider.name(), "dotenv");
let Err(error) = Box::<dyn Provider>::try_from("file") else {
panic!("file should require an explicit directory");
};
assert!(
error
.to_string()
.contains("requires an explicit relative or absolute directory path")
);
match Box::<dyn Provider>::try_from("onepassword") {
Ok(provider) => assert_eq!(provider.name(), "onepassword"),
Err(e) => panic!("Failed to create onepassword provider: {e}"),
}
let provider = Box::<dyn Provider>::try_from("lastpass").unwrap();
assert_eq!(provider.name(), "lastpass");
let provider = Box::<dyn Provider>::try_from("dashlane").unwrap();
assert_eq!(provider.name(), "dashlane");
let provider = Box::<dyn Provider>::try_from("gopass").unwrap();
assert_eq!(provider.name(), "gopass");
let provider = Box::<dyn Provider>::try_from("pass").unwrap();
assert_eq!(provider.name(), "pass");
let provider = Box::<dyn Provider>::try_from("protonpass").unwrap();
assert_eq!(provider.name(), "protonpass");
}
#[test]
fn test_create_from_string_with_colon() {
let provider = Box::<dyn Provider>::try_from("env:").unwrap();
assert_eq!(provider.name(), "env");
let provider = Box::<dyn Provider>::try_from("keyring:").unwrap();
assert_eq!(provider.name(), "keyring");
}
#[test]
fn test_invalid_onepassword_scheme() {
let result = Box::<dyn Provider>::try_from("1password");
match result {
Err(err) => assert!(err.to_string().contains("Use 'onepassword' instead")),
Ok(_) => panic!("Expected error for '1password' scheme"),
}
let result = Box::<dyn Provider>::try_from("1password:");
match result {
Err(err) => assert!(err.to_string().contains("Use 'onepassword' instead")),
Ok(_) => panic!("Expected error for '1password:' scheme"),
}
let result = Box::<dyn Provider>::try_from("1password://Private");
match result {
Err(err) => assert!(err.to_string().contains("Use 'onepassword' instead")),
Ok(_) => panic!("Expected error for '1password://' scheme"),
}
}
#[test]
fn test_dotenv_with_custom_path() {
let provider = Box::<dyn Provider>::try_from("dotenv://custom/path/to/.env").unwrap();
assert_eq!(provider.name(), "dotenv");
let provider = Box::<dyn Provider>::try_from("dotenv:///custom/path/.env").unwrap();
assert_eq!(provider.name(), "dotenv");
}
#[test]
fn test_unknown_provider() {
let result = Box::<dyn Provider>::try_from("unknown");
assert!(result.is_err());
match result {
Err(crate::MonosecretError::ProviderNotFound(scheme)) => {
assert_eq!(scheme, "unknown");
}
_ => panic!("Expected ProviderNotFound error"),
}
}
#[test]
fn test_dotenv_shorthand_from_docs() {
let provider = Box::<dyn Provider>::try_from("dotenv:.env.production").unwrap();
assert_eq!(provider.name(), "dotenv");
}
#[test]
fn test_documentation_examples() {
let provider = Box::<dyn Provider>::try_from("onepassword://work@Production").unwrap();
assert_eq!(provider.name(), "onepassword");
let provider = Box::<dyn Provider>::try_from("dotenv:/path/to/.env").unwrap();
assert_eq!(provider.name(), "dotenv");
let provider = Box::<dyn Provider>::try_from("lastpass://folder").unwrap();
assert_eq!(provider.name(), "lastpass");
let provider = Box::<dyn Provider>::try_from("dotenv://path").unwrap();
assert_eq!(provider.name(), "dotenv");
let provider = Box::<dyn Provider>::try_from("file:./.secrets").unwrap();
assert_eq!(provider.name(), "file");
assert_eq!(provider.uri(), "file://./.secrets");
let provider = Box::<dyn Provider>::try_from("pass://").unwrap();
assert_eq!(provider.name(), "pass");
let provider = Box::<dyn Provider>::try_from("dashlane://note").unwrap();
assert_eq!(provider.name(), "dashlane");
assert_eq!(provider.uri(), "dashlane://note");
}
#[test]
fn test_edge_cases_and_normalization() {
let provider = Box::<dyn Provider>::try_from("keyring:").unwrap();
assert_eq!(provider.name(), "keyring");
let provider = Box::<dyn Provider>::try_from("dotenv:/absolute/path").unwrap();
assert_eq!(provider.name(), "dotenv");
let Err(err) = Box::<dyn Provider>::try_from("env://localhost") else {
panic!("env authority must be rejected");
};
assert!(err.to_string().contains("ref = { item ="), "{err}");
}
#[test]
fn test_onepassword_uri_forms() {
let provider = Box::<dyn Provider>::try_from("onepassword://Production").unwrap();
assert_eq!(provider.name(), "onepassword");
let Err(err) = Box::<dyn Provider>::try_from("op://Production/db/password") else {
panic!("op:// provider spec must be rejected");
};
assert!(
err.to_string()
.contains("ref = { vault = \"Production\", item = \"db\", field = \"password\" }"),
"{err}"
);
assert!(Box::<dyn Provider>::try_from("onepassword://vault/Production").is_err());
}
#[test]
fn test_url_parsing_behavior() {
use url::Url;
let url = "onepassword://vault/Production".parse::<Url>().unwrap();
assert_eq!(url.scheme(), "onepassword");
assert_eq!(url.host_str(), Some("vault"));
assert_eq!(url.path(), "/Production");
let url = "dotenv://path/to/.env".parse::<Url>().unwrap();
assert_eq!(url.scheme(), "dotenv");
assert_eq!(url.host_str(), Some("path"));
assert_eq!(url.path(), "/to/.env");
}
#[test]
fn test_onepassword_vault_name_with_spaces() {
let provider = Box::<dyn Provider>::try_from("onepassword://Home%20Lab").unwrap();
assert_eq!(provider.name(), "onepassword");
assert_eq!(provider.uri(), "onepassword://Home%20Lab");
let provider = Box::<dyn Provider>::try_from("onepassword://Home Lab").unwrap();
assert_eq!(provider.name(), "onepassword");
assert_eq!(provider.uri(), "onepassword://Home%20Lab");
let provider = Box::<dyn Provider>::try_from("onepassword://work@Home Lab").unwrap();
assert_eq!(provider.name(), "onepassword");
assert_eq!(provider.uri(), "onepassword://work@Home%20Lab");
}
#[test]
fn test_provider_names_with_special_characters() {
let provider = Box::<dyn Provider>::try_from("pass://My Secrets/app").unwrap();
assert_eq!(provider.name(), "pass");
let provider = Box::<dyn Provider>::try_from("keyring://My App/{profile}/{key}").unwrap();
assert_eq!(provider.name(), "keyring");
let provider = Box::<dyn Provider>::try_from("lastpass://Shared Items/dev").unwrap();
assert_eq!(provider.name(), "lastpass");
let provider = Box::<dyn Provider>::try_from("pass://My%20Secrets/app").unwrap();
assert_eq!(provider.name(), "pass");
}
#[cfg(test)]
mod integration_tests {
use super::*;
fn generate_test_project_name() -> String {
use std::time::SystemTime;
use std::time::UNIX_EPOCH;
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_micros();
let suffix = timestamp % 100_000;
format!("monosecret_test_{suffix}")
}
fn get_test_providers() -> Vec<String> {
std::env::var("MONOSECRET_TEST_PROVIDERS")
.unwrap_or_else(|_| String::new())
.split(',')
.filter(|s| !s.is_empty())
.map(|s| s.trim().to_string())
.collect()
}
fn create_provider_with_temp_path(provider_name: &str) -> (Box<dyn Provider>, Option<TempDir>) {
match provider_name {
"dotenv" => {
let temp_dir = TempDir::new().expect("Create temp directory");
let dotenv_path = temp_dir.path().join(".env");
let provider_spec = format!("dotenv:{}", dotenv_path.to_str().unwrap());
let provider = Box::<dyn Provider>::try_from(provider_spec.as_str())
.expect("Should create dotenv provider with path");
(provider, Some(temp_dir))
}
"file" => {
let temp_dir = TempDir::new().expect("Create temp directory");
let provider_spec = format!("file:{}", temp_dir.path().display());
let provider = Box::<dyn Provider>::try_from(provider_spec.as_str())
.expect("Should create file provider with path");
(provider, Some(temp_dir))
}
"pass" => {
let provider =
Box::<dyn Provider>::try_from("pass").expect("Should create pass provider");
(provider, None)
}
#[cfg(feature = "vault")]
"vault" | "vault-kv1" => {
let provider_spec = if provider_name == "vault-kv1" {
"vault://127.0.0.1:8200/kv1?tls=false&kv=1"
} else {
"vault://127.0.0.1:8200?tls=false"
};
let provider = Box::<dyn Provider>::try_from(provider_spec)
.expect("Should create vault provider");
(provider, None)
}
#[cfg(feature = "openbao")]
"openbao" | "openbao-kv1" => {
let provider_spec = if provider_name == "openbao-kv1" {
"openbao://127.0.0.1:8200/kv1?tls=false&kv=1"
} else {
"openbao://127.0.0.1:8200?tls=false"
};
let provider = Box::<dyn Provider>::try_from(provider_spec)
.expect("Should create openbao provider");
(provider, None)
}
#[cfg(feature = "infisical")]
"infisical" => {
let project = std::env::var("INFISICAL_TEST_PROJECT").expect(
"Testing the infisical provider requires a real project: set INFISICAL_TEST_PROJECT to a project UUID (and authenticate via INFISICAL_CLIENT_ID/INFISICAL_CLIENT_SECRET or INFISICAL_TOKEN).",
);
let host = std::env::var("INFISICAL_TEST_HOST")
.unwrap_or_else(|_| "app.infisical.com".to_string());
let env = std::env::var("INFISICAL_TEST_ENV").unwrap_or_else(|_| "dev".to_string());
let tls = if host.starts_with("localhost") || host.starts_with("127.0.0.1") {
"&tls=false"
} else {
""
};
let provider_spec = format!("infisical://{host}/{project}?env={env}{tls}");
let provider = Box::<dyn Provider>::try_from(provider_spec.as_str())
.expect("Should create infisical provider");
(provider, None)
}
#[cfg(feature = "setec")]
"setec" => {
let provider_spec = std::env::var("SETEC_TEST_SERVER").expect(
"Testing the setec provider requires SETEC_TEST_SERVER, for example setec://secrets.example.ts.net",
);
let provider = Box::<dyn Provider>::try_from(provider_spec.as_str())
.expect("SETEC_TEST_SERVER should be a valid setec provider URI");
(provider, None)
}
#[cfg(feature = "akv")]
"akv" => {
let vault = std::env::var("AKV_TEST_VAULT").expect(
"Testing the akv provider requires a real Key Vault: set AKV_TEST_VAULT to a vault name (and authenticate via AZURE_TENANT_ID/AZURE_CLIENT_ID/AZURE_CLIENT_SECRET or `az login`).",
);
let provider_spec = format!("akv://{vault}");
let provider = Box::<dyn Provider>::try_from(provider_spec.as_str())
.expect("Should create akv provider");
(provider, None)
}
#[cfg(feature = "aac")]
"aac" => {
let store = std::env::var("AAC_TEST_STORE").expect(
"Testing the aac provider requires a real store: set AAC_TEST_STORE to a store name (and authenticate via AZURE_TENANT_ID/AZURE_CLIENT_ID/AZURE_CLIENT_SECRET or `az login`).",
);
let mut provider_spec = format!("aac://{store}");
if let Ok(label) = std::env::var("AAC_TEST_LABEL")
&& !label.is_empty()
{
provider_spec.push_str("?label=");
provider_spec.push_str(&crate::provider::ProviderUrl::encode_query(&label));
}
let provider = Box::<dyn Provider>::try_from(provider_spec.as_str())
.expect("Should create aac provider");
(provider, None)
}
#[cfg(feature = "doppler")]
"doppler" => {
let project = std::env::var("DOPPLER_TEST_PROJECT").expect(
"Testing the doppler provider requires a throwaway project: set DOPPLER_TEST_PROJECT to its name (and authenticate via DOPPLER_TOKEN). These tests write, so they never guess a project.",
);
let provider_spec = format!("doppler://{project}");
let provider = Box::<dyn Provider>::try_from(provider_spec.as_str())
.expect("Should create doppler provider");
(provider, None)
}
_ => {
let provider = Box::<dyn Provider>::try_from(provider_name)
.unwrap_or_else(|_| panic!("{provider_name} provider should exist"));
(provider, None)
}
}
}
fn test_provider_basic_workflow(provider: &dyn Provider, provider_name: &str) {
let project_name = generate_test_project_name();
let result = provider.get(Address::convention(
&project_name,
"default",
"TEST_PASSWORD",
));
match result {
Ok(None) | Err(_) => {
}
Ok(Some(_)) => {
panic!("[{provider_name}] Should not find non-existent secret");
}
}
let test_value = SecretBytes::from_utf8(format!("test_password_{provider_name}"));
let writable = provider
.check_writable(Address::convention("proj", "default", "KEY"))
.is_ok();
if writable {
provider
.set(
Address::convention(&project_name, "default", "TEST_PASSWORD"),
&test_value,
)
.unwrap_or_else(|_| {
panic!("[{provider_name}] Provider claims to support set but failed")
});
let retrieved = provider
.get(Address::convention(
&project_name,
"default",
"TEST_PASSWORD",
))
.unwrap_or_else(|_| {
panic!("[{provider_name}] Should not error when getting after set")
});
match retrieved {
Some(value) => {
assert_eq!(
value.expose_secret(),
test_value.expose_secret(),
"[{provider_name}] Retrieved value should match set value"
);
}
None => {
panic!("[{provider_name}] Should find secret after setting it");
}
}
} else {
match provider.set(
Address::convention(&project_name, "default", "TEST_PASSWORD"),
&test_value,
) {
Ok(()) => {
panic!("[{provider_name}] Read-only provider should not allow set operations");
}
Err(_) => {
println!("[{provider_name}] Read-only provider correctly rejected set");
}
}
}
let addr = Address::convention(&project_name, "default", "TEST_PASSWORD");
match provider.delete(addr) {
Ok(removed) => {
assert_eq!(
removed, writable,
"[{provider_name}] delete must report whether an entry was removed"
);
assert!(
provider.get(addr).unwrap_or(None).is_none(),
"[{provider_name}] the secret must be gone after delete"
);
assert!(
!provider.delete(addr).unwrap_or_else(|error| {
panic!("[{provider_name}] deleting an absent entry must not fail: {error}")
}),
"[{provider_name}] deleting an absent entry removes nothing"
);
}
Err(error) => {
assert!(
error.to_string().contains("does not support deleting"),
"[{provider_name}] unexpected delete failure: {error}"
);
}
}
}
#[test]
fn test_all_providers_basic_workflow() {
println!("Testing MockProvider");
let mock = MockProvider::new();
test_provider_basic_workflow(&mock, "mock");
let providers = get_test_providers();
for provider_name in providers {
println!("Testing provider: {provider_name}");
let (provider, _temp_dir) = create_provider_with_temp_path(&provider_name);
test_provider_basic_workflow(provider.as_ref(), &provider_name);
}
}
#[cfg(feature = "keyring")]
#[test]
fn keyring_round_trips_binary_values() {
if !get_test_providers().iter().any(|name| name == "keyring") {
return;
}
let (provider, _temp_dir) = create_provider_with_temp_path("keyring");
let project = generate_test_project_name();
let addr = Address::convention(&project, "default", "BINARY");
let expected = SecretBytes::from_slice(b"\0\xff\x80\r\n");
provider.set(addr, &expected).unwrap();
let actual = provider.get(addr);
let deleted = provider.delete(addr);
assert_eq!(actual.unwrap(), Some(expected));
assert!(deleted.unwrap());
}
#[test]
fn gopass_keeps_text_entries_and_round_trips_binary_values() {
if !get_test_providers().iter().any(|name| name == "gopass") {
return;
}
let (provider, _temp_dir) = create_provider_with_temp_path("gopass");
let project = generate_test_project_name();
let text = Address::convention(&project, "default", "TEXT");
provider
.set(text, &SecretBytes::from_utf8("hunter2"))
.unwrap();
let shown = std::process::Command::new("gopass")
.args(["show", "-y", "-o"])
.arg(format!("monosecret/{project}/default/TEXT"))
.output()
.unwrap();
let text_read = provider.get(text);
assert!(provider.delete(text).unwrap());
assert!(shown.status.success(), "{shown:?}");
assert_eq!(shown.stdout, b"hunter2");
assert_eq!(text_read.unwrap(), Some(SecretBytes::from_utf8("hunter2")));
let cases: [&[u8]; 4] = [b"line1\nline2\n", b" padded ", b"a\r\nb", b"\0\xff\x80\r\n"];
for (index, expected) in cases.into_iter().enumerate() {
let key = format!("BINARY_{index}");
let addr = Address::convention(&project, "default", &key);
let expected = SecretBytes::from_slice(expected);
provider.set(addr, &expected).unwrap();
let actual = provider.get(addr);
let rewritten = provider.set(addr, &expected);
let deleted = provider.delete(addr);
assert_eq!(actual.unwrap(), Some(expected), "{key}");
rewritten.unwrap();
assert!(deleted.unwrap());
}
}
#[cfg(feature = "infisical")]
#[test]
fn test_infisical_refuses_a_withheld_value() {
let (Ok(client_id), Ok(client_secret)) = (
std::env::var("INFISICAL_TEST_NOREAD_CLIENT_ID"),
std::env::var("INFISICAL_TEST_NOREAD_CLIENT_SECRET"),
) else {
eprintln!(
"skipping: set INFISICAL_TEST_NOREAD_CLIENT_ID/SECRET to an identity that may \
see a secret exists but not read it (needs an Infisical custom role)"
);
return;
};
if !get_test_providers().iter().any(|p| p == "infisical") {
eprintln!("skipping: MONOSECRET_TEST_PROVIDERS does not name infisical");
return;
}
if std::env::var("INFISICAL_TOKEN").is_ok() {
eprintln!(
"skipping: INFISICAL_TOKEN outranks the restricted identity's credentials. \
Unset it and authenticate with INFISICAL_CLIENT_ID/INFISICAL_CLIENT_SECRET \
to exercise a withheld value."
);
return;
}
let project_name = generate_test_project_name();
let (writer, _t) = create_provider_with_temp_path("infisical");
writer
.set(
Address::convention(&project_name, "default", "HIDDEN_KEY"),
&SecretBytes::from_utf8("plaintext"),
)
.expect("the writing identity should store a secret");
let (mut restricted, _t) = create_provider_with_temp_path("infisical");
let mut credentials = crate::provider::ProviderCredentials::new();
credentials.insert("client_id".to_string(), SecretBytes::from_utf8(client_id));
credentials.insert(
"client_secret".to_string(),
SecretBytes::from_utf8(client_secret),
);
restricted.with_credentials(credentials);
let err = restricted
.get(Address::convention(&project_name, "default", "HIDDEN_KEY"))
.expect_err("a withheld value must not read as a secret");
assert!(
err.to_string().contains("withheld"),
"the refusal should say the value was withheld, got: {err}"
);
assert!(
!err.to_string().contains("plaintext"),
"the error must not carry the value"
);
}
#[test]
fn test_all_providers_isolate_profiles() {
let mock = MockProvider::new();
test_provider_profile_isolation(&mock, "mock");
for provider_name in get_test_providers() {
println!("Testing provider: {provider_name}");
let (provider, _temp_dir) = create_provider_with_temp_path(&provider_name);
test_provider_profile_isolation(provider.as_ref(), &provider_name);
}
}
const FLAT_PROVIDERS: &[&str] = &["dotenv", "env"];
fn test_provider_profile_isolation(provider: &dyn Provider, provider_name: &str) {
if FLAT_PROVIDERS.contains(&provider_name)
|| provider
.check_writable(Address::convention("proj", "default", "KEY"))
.is_err()
{
return;
}
let project_name = generate_test_project_name();
let profiles = ["dev", "staging", "prod"];
for profile in profiles {
let value = SecretBytes::from_utf8(format!("value_for_{profile}"));
provider
.set(
Address::convention(&project_name, profile, "API_KEY"),
&value,
)
.unwrap_or_else(|e| panic!("[{provider_name}] set under '{profile}': {e}"));
}
for profile in profiles {
let found = provider
.get(Address::convention(&project_name, profile, "API_KEY"))
.unwrap_or_else(|e| panic!("[{provider_name}] get under '{profile}': {e}"))
.unwrap_or_else(|| panic!("[{provider_name}] '{profile}' lost its secret"));
assert_eq!(
found.expose_secret(),
format!("value_for_{profile}").as_bytes(),
"[{provider_name}] profile '{profile}' reads another profile's value"
);
}
}
#[test]
fn test_provider_special_characters() {
let test_cases = vec![
("SPACED_VALUE", "value with spaces"),
("NEWLINE_VALUE", "value\nwith\nnewlines"),
("SPECIAL_CHARS", "!@#%^&*()_+-=[]{}|;',./<>?"),
("UNICODE_VALUE", "🔐 Secret with émojis and ñ"),
];
let provider = MockProvider::new();
let project_name = generate_test_project_name();
for (key, value) in &test_cases {
let secret_value = SecretBytes::from_utf8(value.to_string());
provider
.set(
Address::convention(&project_name, "default", key),
&secret_value,
)
.expect("Mock provider should handle all characters");
let result = provider
.get(Address::convention(&project_name, "default", key))
.expect("Should not error when getting");
assert_eq!(
result.map(|secret| secret.try_as_utf8().unwrap().to_string()),
Some(value.to_string()),
"Special characters should be preserved"
);
}
}
#[test]
fn test_provider_profile_support() {
let provider = MockProvider::new();
let project_name = generate_test_project_name();
let profiles = vec!["dev", "staging", "prod"];
let test_key = "API_KEY";
for profile in &profiles {
let value = SecretBytes::from_utf8(format!("key_for_{profile}"));
provider
.set(
Address::convention(&project_name, profile, test_key),
&value,
)
.expect("Should set with profile");
let result = provider
.get(Address::convention(&project_name, profile, test_key))
.expect("Should get with profile");
assert_eq!(
result.map(|secret| secret.try_as_utf8().unwrap().to_string()),
Some(value.try_as_utf8().unwrap().to_string()),
"Profile-specific value should match"
);
}
for profile in profiles {
let result = provider
.get(Address::convention(&project_name, profile, test_key))
.expect("Should not error");
let expected_value = format!("key_for_{profile}");
assert_eq!(
result.map(|secret| secret.try_as_utf8().unwrap().to_string()),
Some(expected_value),
"Should find profile-specific value"
);
}
}
#[test]
fn test_default_reflect_returns_error() {
let provider = MockProvider::new();
let result = provider.reflect(DiscoveryContext::new("project", "default"));
assert!(
result.is_err(),
"Default reflect implementation should return an error"
);
let error = result.unwrap_err();
let error_msg = error.to_string();
assert!(
error_msg.contains("does not support reflection"),
"Error message should indicate reflection is not supported"
);
}
#[test]
fn test_pass_provider_creation() {
let provider = Box::<dyn Provider>::try_from("pass").unwrap();
assert_eq!(provider.name(), "pass");
assert_eq!(provider.uri(), "pass");
let provider = Box::<dyn Provider>::try_from("pass://").unwrap();
assert_eq!(provider.name(), "pass");
assert_eq!(provider.uri(), "pass");
}
#[test]
fn test_keyring_with_folder_prefix() {
let provider =
Box::<dyn Provider>::try_from("keyring://monosecret/shared/{profile}/{key}").unwrap();
assert_eq!(provider.name(), "keyring");
assert_eq!(
provider.uri(),
"keyring://monosecret/shared/{profile}/{key}"
);
let provider = Box::<dyn Provider>::try_from("keyring://").unwrap();
assert_eq!(provider.name(), "keyring");
assert_eq!(provider.uri(), "keyring");
}
#[test]
fn test_lastpass_with_folder_prefix() {
let provider =
Box::<dyn Provider>::try_from("lastpass://Work/Monosecret/{profile}/{key}").unwrap();
assert_eq!(provider.name(), "lastpass");
assert_eq!(provider.uri(), "lastpass://Work/Monosecret/{profile}/{key}");
let provider = Box::<dyn Provider>::try_from("lastpass://").unwrap();
assert_eq!(provider.name(), "lastpass");
assert_eq!(provider.uri(), "lastpass");
}
#[test]
fn test_pass_with_folder_prefix() {
let provider =
Box::<dyn Provider>::try_from("pass://monosecret/shared/{profile}/{key}").unwrap();
assert_eq!(provider.name(), "pass");
assert_eq!(provider.uri(), "pass://monosecret/shared/{profile}/{key}");
let provider = Box::<dyn Provider>::try_from("pass://").unwrap();
assert_eq!(provider.name(), "pass");
assert_eq!(provider.uri(), "pass");
}
#[test]
fn test_pass_provider_is_writable() {
let provider = Box::<dyn Provider>::try_from("pass").unwrap();
assert!(
provider
.check_writable(Address::convention("proj", "default", "KEY"))
.is_ok(),
"Pass provider should support write operations"
);
}
#[test]
fn test_protonpass_provider_creation() {
let provider = Box::<dyn Provider>::try_from("protonpass").unwrap();
assert_eq!(provider.name(), "protonpass");
assert_eq!(provider.uri(), "protonpass");
let provider = Box::<dyn Provider>::try_from("protonpass://").unwrap();
assert_eq!(provider.name(), "protonpass");
assert_eq!(provider.uri(), "protonpass");
let provider = Box::<dyn Provider>::try_from("protonpass://Work").unwrap();
assert_eq!(provider.name(), "protonpass");
assert_eq!(provider.uri(), "protonpass://Work");
let provider =
Box::<dyn Provider>::try_from("protonpass://Work/{project}/{profile}/{key}").unwrap();
assert_eq!(provider.name(), "protonpass");
assert_eq!(
provider.uri(),
"protonpass://Work/{project}/{profile}/{key}"
);
}
#[test]
fn test_protonpass_provider_is_writable() {
let provider = Box::<dyn Provider>::try_from("protonpass").unwrap();
assert!(
provider
.check_writable(Address::convention("proj", "default", "KEY"))
.is_ok(),
"ProtonPass provider should support write operations"
);
}
#[cfg(feature = "awssm")]
#[test]
#[allow(clippy::indexing_slicing)] fn test_awssm_batch_get() {
let providers = get_test_providers();
if !providers.contains(&"awssm".to_string()) {
return;
}
let (provider, _temp_dir) = create_provider_with_temp_path("awssm");
let project_name = generate_test_project_name();
let profile = "default";
let test_secrets = vec![
("BATCH_TEST_1", "value1"),
("BATCH_TEST_2", "value2"),
("BATCH_TEST_3", "value3"),
];
for (key, value) in &test_secrets {
provider
.set(
Address::convention(&project_name, profile, key),
&SecretBytes::from_utf8(value.to_string()),
)
.unwrap();
}
let keys = [
"BATCH_TEST_1",
"BATCH_TEST_2",
"BATCH_TEST_3",
"NONEXISTENT",
];
let requests: Vec<(&str, Address<'_>)> = keys
.iter()
.map(|key| (*key, Address::convention(&project_name, profile, key)))
.collect();
let result = provider.get_many(&requests).unwrap();
assert_eq!(result.len(), 3);
assert_eq!(result["BATCH_TEST_1"].expose_secret(), b"value1");
assert_eq!(result["BATCH_TEST_2"].expose_secret(), b"value2");
assert_eq!(result["BATCH_TEST_3"].expose_secret(), b"value3");
assert!(!result.contains_key("NONEXISTENT"));
}
#[cfg(feature = "awsps")]
#[test]
#[allow(clippy::indexing_slicing)] fn test_awsps_batch_get() {
let providers = get_test_providers();
if !providers.contains(&"awsps".to_string()) {
return;
}
let (provider, _temp_dir) = create_provider_with_temp_path("awsps");
let project_name = generate_test_project_name();
let profile = "default";
let test_parameters = [
("BATCH_TEST_1", "value1"),
("BATCH_TEST_2", "value2"),
("BATCH_TEST_3", "value3"),
];
for (key, value) in test_parameters {
provider
.set(
Address::convention(&project_name, profile, key),
&SecretBytes::from_utf8(value.to_string()),
)
.unwrap();
}
let keys = [
"BATCH_TEST_1",
"BATCH_TEST_2",
"BATCH_TEST_3",
"NONEXISTENT",
];
let requests: Vec<(&str, Address<'_>)> = keys
.iter()
.map(|key| (*key, Address::convention(&project_name, profile, key)))
.collect();
let result = provider.get_many(&requests).unwrap();
assert_eq!(result.len(), 3);
assert_eq!(result["BATCH_TEST_1"].expose_secret(), b"value1");
assert_eq!(result["BATCH_TEST_2"].expose_secret(), b"value2");
assert_eq!(result["BATCH_TEST_3"].expose_secret(), b"value3");
assert!(!result.contains_key("NONEXISTENT"));
}
#[cfg(feature = "doppler")]
fn doppler_provider_with_token(
project: &str,
config: Option<&str>,
token: &str,
) -> crate::provider::doppler::DopplerProvider {
use crate::provider::doppler::DopplerConfig;
use crate::provider::doppler::DopplerProvider;
let mut provider = DopplerProvider::new(DopplerConfig {
project: project.to_string(),
config: config.map(str::to_string),
});
let mut credentials = crate::provider::ProviderCredentials::new();
credentials.insert("token".to_string(), SecretBytes::from_utf8(token));
provider.with_credentials(credentials);
provider
}
#[cfg(feature = "doppler")]
fn doppler_test_project() -> Option<String> {
if !get_test_providers().contains(&"doppler".to_string()) {
return None;
}
Some(std::env::var("DOPPLER_TEST_PROJECT").expect(
"Testing the doppler provider requires DOPPLER_TEST_PROJECT to name a throwaway project.",
))
}
#[cfg(feature = "doppler")]
#[test]
fn test_doppler_batch_get() {
if doppler_test_project().is_none() {
return;
}
let provider = create_provider_with_temp_path("doppler").0;
let profile = "dev";
let stored = [
("SECRETSPEC_BATCH_1", "value1"),
("SECRETSPEC_BATCH_2", "value2"),
("SECRETSPEC_BATCH_3", "value3"),
];
for (key, value) in stored {
provider
.set(
Address::convention("unused", profile, key),
&SecretBytes::from_utf8(value),
)
.expect("write a batch secret");
}
let keys = [
"SECRETSPEC_BATCH_1",
"SECRETSPEC_BATCH_2",
"SECRETSPEC_BATCH_3",
"SECRETSPEC_BATCH_NONEXISTENT",
];
let requests: Vec<(&str, Address<'_>)> = keys
.iter()
.map(|key| (*key, Address::convention("unused", profile, key)))
.collect();
let result = provider.get_many(&requests).expect("batch read");
for (key, value) in stored {
assert_eq!(
result.get(key).map(SecretBytes::expose_secret),
Some(value.as_bytes()),
"{key} was not returned by the batch read"
);
}
assert!(
!result.contains_key("SECRETSPEC_BATCH_NONEXISTENT"),
"a name the config does not hold must be absent, not an error"
);
assert_eq!(result.len(), stored.len());
for (key, _) in stored {
provider
.delete(Address::convention("unused", profile, key))
.expect("clean up");
}
}
#[cfg(feature = "doppler")]
#[test]
fn test_doppler_filters_reserved_names() {
let Some(project) = doppler_test_project() else {
return;
};
let provider = create_provider_with_temp_path("doppler").0;
let reserved = crate::provider::doppler::RESERVED_NAMES;
let requests: Vec<(&str, Address<'_>)> = reserved
.iter()
.map(|name| (*name, Address::convention("unused", "dev", name)))
.collect();
let batch = provider.get_many(&requests).expect("batch read");
assert!(
batch.is_empty(),
"Doppler's own injected names must not be served as secrets: {:?}",
batch.keys().collect::<Vec<_>>()
);
for name in reserved {
assert!(
provider
.get(Address::convention("unused", "dev", name))
.expect("single read")
.is_none(),
"{name} must read as missing, exactly as it does in a batch"
);
}
let token = std::env::var("DOPPLER_TOKEN").expect("DOPPLER_TOKEN");
let pinned = doppler_provider_with_token(&project, Some("dev"), &token);
let reflected = pinned
.reflect(DiscoveryContext::new("unused", "dev"))
.expect("reflect the config");
for name in reserved {
assert!(
!reflected.contains_key(name),
"{name} must not be offered for discovery"
);
}
}
#[cfg(feature = "doppler")]
#[test]
fn test_doppler_pinned_service_tokens_refuse_other_coordinates() {
let Some(project) = doppler_test_project() else {
return;
};
let pinned_tokens = [
("dev", "DOPPLER_ST_DEV_TOKEN"),
("staging", "DOPPLER_ST_STAGING_TOKEN"),
("prod", "DOPPLER_ST_PROD_TOKEN"),
];
let mut exercised = 0;
for (config, var) in pinned_tokens {
let Ok(token) = std::env::var(var) else {
eprintln!("skipping {var}: not set");
continue;
};
exercised += 1;
let matching = doppler_provider_with_token(&project, Some(config), &token);
matching
.get(Address::convention("unused", "ignored", "NO_SUCH_SECRET"))
.unwrap_or_else(|error| {
panic!("[{var}] a token must reach the config it is pinned to: {error}")
});
let other_config = if config == "dev" { "prod" } else { "dev" };
let mismatched = doppler_provider_with_token(&project, Some(other_config), &token);
let error = mismatched
.get(Address::convention("unused", "ignored", "NO_SUCH_SECRET"))
.expect_err("a pinned token must not serve another config")
.to_string();
assert!(
error.contains("does not have access to requested config"),
"[{var}] expected Doppler's config refusal, got: {error}"
);
assert!(
error.contains(other_config),
"[{var}] the refusal must name the config refused: {error}"
);
let elsewhere = doppler_provider_with_token(
"monosecret-provider-ci-does-not-exist",
Some(config),
&token,
);
let error = elsewhere
.get(Address::convention("unused", "ignored", "NO_SUCH_SECRET"))
.expect_err("a pinned token must not serve another project")
.to_string();
assert!(
error.contains("does not have access to requested project"),
"[{var}] expected Doppler's project refusal, got: {error}"
);
}
if exercised == 0 {
eprintln!(
"skipping the pinned-token paths: set DOPPLER_ST_DEV_TOKEN, \
DOPPLER_ST_STAGING_TOKEN or DOPPLER_ST_PROD_TOKEN to a dp.st. token \
pinned to that config to exercise them"
);
}
}
#[cfg(feature = "awssm")]
#[test]
fn test_awssm_provider_creation() {
let provider = Box::<dyn Provider>::try_from("awssm://us-east-1").unwrap();
assert_eq!(provider.name(), "awssm");
assert_eq!(provider.uri(), "awssm://us-east-1");
}
#[cfg(feature = "awssm")]
#[test]
fn test_awssm_provider_creation_without_region() {
let provider = Box::<dyn Provider>::try_from("awssm://").unwrap();
assert_eq!(provider.name(), "awssm");
assert_eq!(provider.uri(), "awssm");
let provider = Box::<dyn Provider>::try_from("awssm").unwrap();
assert_eq!(provider.name(), "awssm");
assert_eq!(provider.uri(), "awssm");
}
#[cfg(feature = "awssm")]
#[test]
fn test_awssm_provider_with_aws_profile() {
let provider = Box::<dyn Provider>::try_from("awssm://production@us-east-1").unwrap();
assert_eq!(provider.name(), "awssm");
assert_eq!(provider.uri(), "awssm://production@us-east-1");
let provider = Box::<dyn Provider>::try_from("awssm://staging@eu-west-1").unwrap();
assert_eq!(provider.name(), "awssm");
assert_eq!(provider.uri(), "awssm://staging@eu-west-1");
}
#[cfg(feature = "awssm")]
#[test]
fn test_awssm_provider_with_prefix() {
let provider = Box::<dyn Provider>::try_from("awssm://us-east-1?prefix=myteam").unwrap();
assert_eq!(provider.name(), "awssm");
assert_eq!(provider.uri(), "awssm://us-east-1?prefix=myteam");
}
#[cfg(feature = "awssm")]
#[test]
fn test_awssm_provider_with_prefix_and_profile() {
let provider =
Box::<dyn Provider>::try_from("awssm://production@us-east-1?prefix=myteam").unwrap();
assert_eq!(provider.name(), "awssm");
assert_eq!(provider.uri(), "awssm://production@us-east-1?prefix=myteam");
}
#[cfg(feature = "awssm")]
#[test]
fn test_awssm_provider_with_prefix_no_region() {
let provider = Box::<dyn Provider>::try_from("awssm://?prefix=myteam").unwrap();
assert_eq!(provider.name(), "awssm");
assert_eq!(provider.uri(), "awssm://?prefix=myteam");
}
#[cfg(feature = "awsps")]
#[test]
fn test_awsps_provider_creation() {
let provider = Box::<dyn Provider>::try_from("awsps://us-east-1").unwrap();
assert_eq!(provider.name(), "awsps");
assert_eq!(provider.uri(), "awsps://us-east-1");
}
#[cfg(feature = "awsps")]
#[test]
fn test_awsps_provider_creation_without_region() {
let provider = Box::<dyn Provider>::try_from("awsps://").unwrap();
assert_eq!(provider.name(), "awsps");
assert_eq!(provider.uri(), "awsps");
let provider = Box::<dyn Provider>::try_from("awsps").unwrap();
assert_eq!(provider.name(), "awsps");
assert_eq!(provider.uri(), "awsps");
}
#[cfg(feature = "awsps")]
#[test]
fn test_awsps_provider_with_profile_and_options() {
let provider = Box::<dyn Provider>::try_from(
"awsps://production@us-east-1?prefix=/team&kms_key_id=alias/parameters&tier=advanced",
)
.unwrap();
assert_eq!(provider.name(), "awsps");
assert_eq!(
provider.uri(),
"awsps://production@us-east-1?prefix=/team&kms_key_id=alias/parameters&tier=advanced"
);
}
#[cfg(feature = "vault")]
#[test]
fn test_vault_provider_creation() {
let provider =
Box::<dyn Provider>::try_from("vault://vault.example.com:8200/secret").unwrap();
assert_eq!(provider.name(), "vault");
}
#[cfg(feature = "vault")]
#[test]
fn test_vault_provider_default_mount() {
let provider = Box::<dyn Provider>::try_from("vault://vault.example.com:8200").unwrap();
assert_eq!(provider.name(), "vault");
}
#[cfg(feature = "vault")]
#[test]
fn test_vault_provider_custom_mount() {
let provider =
Box::<dyn Provider>::try_from("vault://vault.example.com:8200/custom-kv").unwrap();
assert_eq!(provider.name(), "vault");
}
#[cfg(feature = "vault")]
#[test]
fn test_vault_provider_kv_v1() {
let provider =
Box::<dyn Provider>::try_from("vault://vault.example.com:8200/secret?kv=1").unwrap();
assert_eq!(provider.name(), "vault");
}
#[cfg(feature = "vault")]
#[test]
fn test_vault_provider_with_namespace() {
let provider =
Box::<dyn Provider>::try_from("vault://ns1@vault.example.com:8200/secret").unwrap();
assert_eq!(provider.name(), "vault");
}
#[cfg(feature = "vault")]
#[test]
fn test_vault_provider_tls_false() {
let provider =
Box::<dyn Provider>::try_from("vault://127.0.0.1:8200/secret?tls=false").unwrap();
assert_eq!(provider.name(), "vault");
}
#[cfg(feature = "openbao")]
#[test]
fn test_openbao_provider_creation() {
let provider = Box::<dyn Provider>::try_from("openbao://bao.internal:8200/secret").unwrap();
assert_eq!(provider.name(), "openbao");
assert_eq!(provider.uri(), "openbao://bao.internal:8200/secret");
}
#[cfg(feature = "vault")]
#[test]
fn test_vault_provider_requires_address() {
let had_vault_addr = std::env::var("VAULT_ADDR").ok();
unsafe {
std::env::remove_var("VAULT_ADDR");
}
let result = Box::<dyn Provider>::try_from("vault://");
assert!(result.is_err(), "Vault provider should require an address");
if let Some(addr) = had_vault_addr {
unsafe {
std::env::set_var("VAULT_ADDR", addr);
}
}
}
#[cfg(feature = "gcsm")]
#[test]
fn test_gcsm_provider_creation() {
let provider = Box::<dyn Provider>::try_from("gcsm://my-project").unwrap();
assert_eq!(provider.name(), "gcsm");
assert_eq!(provider.uri(), "gcsm://my-project");
}
#[cfg(feature = "gcsm")]
#[test]
fn test_gcsm_provider_requires_project_id() {
let result = Box::<dyn Provider>::try_from("gcsm://");
assert!(result.is_err(), "GCSM provider should require project ID");
let result = Box::<dyn Provider>::try_from("gcsm");
assert!(result.is_err(), "GCSM provider should require project ID");
}
#[cfg(feature = "bws")]
#[test]
fn test_bws_provider_creation() {
let provider =
Box::<dyn Provider>::try_from("bws://a9230ec4-5507-4870-b8b5-b3f500587e4c").unwrap();
assert_eq!(provider.name(), "bws");
assert_eq!(provider.uri(), "bws://a9230ec4-5507-4870-b8b5-b3f500587e4c");
}
#[cfg(feature = "bws")]
#[test]
fn test_bws_provider_requires_project_id() {
let result = Box::<dyn Provider>::try_from("bws://");
assert!(result.is_err());
let result = Box::<dyn Provider>::try_from("bws");
assert!(result.is_err());
}
#[cfg(feature = "bws")]
#[test]
fn test_bws_provider_validates_uuid_format() {
let result = Box::<dyn Provider>::try_from("bws://not-a-uuid");
assert!(result.is_err());
let result = Box::<dyn Provider>::try_from("bws://12345");
assert!(result.is_err());
}
#[cfg(feature = "bw")]
#[test]
fn test_bw_provider_creation() {
let provider = Box::<dyn Provider>::try_from("bw://").unwrap();
assert_eq!(provider.name(), "bw");
assert_eq!(provider.uri(), "bw://");
}
#[cfg(feature = "bw")]
#[test]
fn test_bw_provider_with_collection() {
let provider = Box::<dyn Provider>::try_from("bw://my-collection").unwrap();
assert_eq!(provider.name(), "bw");
assert_eq!(provider.uri(), "bw://my-collection");
}
#[cfg(feature = "bw")]
#[test]
fn test_bw_provider_with_org_collection() {
let provider = Box::<dyn Provider>::try_from("bw://myorg@dev-secrets").unwrap();
assert_eq!(provider.name(), "bw");
assert_eq!(provider.uri(), "bw://myorg@dev-secrets");
}
#[cfg(feature = "bw")]
#[test]
fn test_bw_provider_rejects_bws_scheme() {
use crate::provider::bw::BitwardenConfig;
let url = crate::provider::ProviderUrl::new(url::Url::parse("bws://project-id").unwrap());
let result = BitwardenConfig::try_from(&url);
assert!(result.is_err());
assert!(result.err().unwrap().to_string().contains("Invalid scheme"));
}
#[cfg(feature = "gcsm")]
#[test]
fn test_gcsm_provider_validates_project_id_format() {
let result = Box::<dyn Provider>::try_from("gcsm://short");
assert!(result.is_err(), "Should reject project ID < 6 chars");
let result = Box::<dyn Provider>::try_from("gcsm://123456");
assert!(
result.is_err(),
"Should reject project ID starting with number"
);
let result = Box::<dyn Provider>::try_from("gcsm://My-Project-123");
assert!(result.is_err(), "Should reject project ID with uppercase");
let result = Box::<dyn Provider>::try_from("gcsm://my-project-");
assert!(
result.is_err(),
"Should reject project ID ending with hyphen"
);
let result = Box::<dyn Provider>::try_from("gcsm://my_project");
assert!(result.is_err(), "Should reject project ID with underscore");
let provider = Box::<dyn Provider>::try_from("gcsm://my-project-123").unwrap();
assert_eq!(provider.name(), "gcsm");
let provider = Box::<dyn Provider>::try_from("gcsm://project123").unwrap();
assert_eq!(provider.name(), "gcsm");
}
#[test]
fn credentials_reach_preflight_wrapped_provider() {
use url::Url;
use crate::provider::ProviderCredentials;
use crate::provider::ProviderUrl;
use crate::provider::provider_from_url;
let _lock = crate::tests::scrub_resolution_env();
let _env = crate::tests::EnvVarGuard::remove("OP_SERVICE_ACCOUNT_TOKEN");
let scope_with = |token: Option<&str>| {
let mut credentials = ProviderCredentials::new();
if let Some(token) = token {
credentials.insert(
"service_account_token".to_string(),
SecretBytes::from_utf8(token),
);
}
let url = ProviderUrl::new(Url::parse("onepassword://Private").unwrap());
provider_from_url(&url, credentials)
.unwrap()
.auth_scope_key()
.expect("onepassword advertises an auth scope")
};
let without_token = scope_with(None);
let with_token = scope_with(Some("tok-xyz"));
assert_ne!(
with_token, without_token,
"provider credential should be injected before Arc-wrapping"
);
assert_eq!(with_token, scope_with(Some("tok-xyz")));
assert_ne!(with_token, scope_with(Some("tok-other")));
assert!(
!with_token.contains("tok-xyz"),
"auth scope key must not embed the plaintext token: {with_token}"
);
}
}
#[cfg(test)]
const HOSTILE_ITEMS: &[&str] = &[
"CACHIX_SIGNING_KEY_cache-a",
"with space",
"1LEADING_DIGIT",
"dotted.name",
"sla/sh",
"_VALID_UNDERSCORE",
"PLAIN_VALID_1",
];
#[cfg(test)]
fn assert_write_read_symmetry(provider: &dyn Provider) {
provider
.set(
Address::convention("proj", "default", "KEEP"),
&SecretBytes::from_utf8("kept"),
)
.unwrap();
for item in HOSTILE_ITEMS {
let addr = crate::config::NativeAddress {
item: (*item).to_string(),
..Default::default()
};
let wrote = provider
.set(Address::Native(&addr), &SecretBytes::from_utf8("v"))
.is_ok();
if wrote {
let got = provider.get(Address::Native(&addr)).unwrap();
assert_eq!(
got.map(|secret| secret.try_as_utf8().unwrap().to_string()),
Some("v".to_string()),
"provider `{}` accepted a write of `{item}` it cannot read back",
provider.name(),
);
}
let kept = provider
.get(Address::convention("proj", "default", "KEEP"))
.unwrap();
assert_eq!(
kept.map(|secret| secret.try_as_utf8().unwrap().to_string()),
Some("kept".to_string()),
"provider `{}`: a write of `{item}` corrupted other secrets",
provider.name(),
);
}
}
#[test]
fn dotenv_write_read_symmetry() {
use super::dotenv::DotEnvConfig;
use super::dotenv::DotEnvProvider;
let dir = TempDir::new().unwrap();
let provider = DotEnvProvider::new(DotEnvConfig {
path: dir.path().join(".env"),
});
assert_write_read_symmetry(&provider);
}
#[test]
fn compiled_provider_cannot_be_shadowed_by_external_discovery() {
use super::ProviderCredentials;
use super::ProviderUrl;
use super::provider_from_url_with_discovery;
let directory = TempDir::new().unwrap();
let url = ProviderUrl::new(url::Url::from_file_path(directory.path().join(".env")).unwrap());
let provider = provider_from_url_with_discovery(&url, ProviderCredentials::new(), |_| {
panic!("external discovery must not run for a compiled provider scheme")
})
.unwrap();
assert_eq!(provider.name(), "file");
}
#[test]
fn file_write_read_symmetry() {
use super::file::FileConfig;
use super::file::FileProvider;
let dir = TempDir::new().unwrap();
let provider = FileProvider::new(FileConfig {
directory: dir.path().to_path_buf(),
});
assert_write_read_symmetry(&provider);
}
#[test]
fn mock_provider_write_read_symmetry() {
assert_write_read_symmetry(&MockProvider::new());
}
struct DeletingProvider;
impl Provider for DeletingProvider {
fn convention_address(
&self,
project: &str,
profile: &str,
key: &str,
) -> Result<crate::config::NativeAddress> {
Ok(crate::config::NativeAddress {
item: format!("{project}/{profile}/{key}"),
..Default::default()
})
}
fn get(&self, _addr: Address<'_>) -> Result<Option<SecretBytes>> {
Ok(None)
}
fn set(&self, _addr: Address<'_>, _value: &SecretBytes) -> Result<()> {
Ok(())
}
fn delete(&self, _addr: Address<'_>) -> Result<bool> {
Ok(true)
}
fn supports_delete(&self) -> bool {
true
}
fn name(&self) -> &'static str {
"deleting"
}
fn uri(&self) -> String {
"deleting://".to_string()
}
}
#[test]
fn check_deletable_refuses_a_provider_that_cannot_delete() {
let provider = CountingProvider::new(&[]);
let addr = Address::convention("proj", "default", "API_KEY");
let err = provider
.check_deletable(addr)
.expect_err("a provider without delete must not pass the deletion preflight");
assert!(
err.to_string()
.contains("does not support deleting secrets"),
"got: {err}"
);
}
#[test]
fn check_deletable_and_delete_refuse_with_the_same_reason() {
let provider = CountingProvider::new(&[]);
let addr = Address::convention("proj", "default", "API_KEY");
let preflight = provider.check_deletable(addr).unwrap_err().to_string();
let attempted = provider.delete(addr).unwrap_err().to_string();
assert_eq!(preflight, attempted);
}
#[test]
fn check_deletable_admits_a_provider_that_opts_in() {
let provider = DeletingProvider;
let addr = Address::convention("proj", "default", "API_KEY");
provider
.check_deletable(addr)
.expect("a provider that implements delete must pass preflight");
}
#[test]
fn providers_do_not_support_deletion_unless_they_say_so() {
assert!(!CountingProvider::new(&[]).supports_delete());
assert!(DeletingProvider.supports_delete());
}
static REVISION_STORE: std::sync::LazyLock<Mutex<HashMap<String, crate::ProviderValue>>> =
std::sync::LazyLock::new(|| Mutex::new(HashMap::new()));
static REVISION_GENERATION: AtomicUsize = AtomicUsize::new(0);
pub(crate) struct RevisionTestProvider;
impl RevisionTestProvider {
fn new(_: MemTestConfig) -> Self {
Self
}
}
crate::register_provider! {
struct: RevisionTestProvider,
config: MemTestConfig,
name: "revisiontest",
description: "Versioned in-memory test provider",
schemes: ["revisiontest"],
examples: ["revisiontest://"],
credential_names: [],
deletes: true,
}
impl Provider for RevisionTestProvider {
fn convention_address(
&self,
project: &str,
profile: &str,
key: &str,
) -> Result<crate::config::NativeAddress> {
MemTestProvider.convention_address(project, profile, key)
}
fn name(&self) -> &str {
Self::PROVIDER_NAME
}
fn uri(&self) -> String {
"revisiontest://".into()
}
fn get(&self, addr: Address<'_>) -> Result<Option<SecretBytes>> {
self.get_with_metadata(addr)
.map(|value| value.map(|value| value.value))
}
fn get_with_metadata(&self, addr: Address<'_>) -> Result<Option<crate::ProviderValue>> {
Ok(REVISION_STORE
.lock()
.unwrap()
.get(super::flat_item(self, addr)?.as_ref())
.cloned())
}
fn get_many_with_metadata(
&self,
requests: &[(&str, Address<'_>)],
) -> Result<HashMap<String, crate::ProviderValue>> {
super::get_each_with(requests, |addr| self.get_with_metadata(addr))
}
fn set(&self, addr: Address<'_>, value: &SecretBytes) -> Result<()> {
let item = super::flat_item(self, addr)?.into_owned();
let mut store = REVISION_STORE.lock().unwrap();
let generation = REVISION_GENERATION.fetch_add(1, Ordering::SeqCst);
let revision =
crate::revision::digest("test-generation", &[&item, &generation.to_string()]);
store.insert(
item,
crate::ProviderValue::new(value.clone(), None).with_revision(Some(revision)),
);
Ok(())
}
fn delete(&self, addr: Address<'_>) -> Result<bool> {
Ok(REVISION_STORE
.lock()
.unwrap()
.remove(super::flat_item(self, addr)?.as_ref())
.is_some())
}
}