use super::format::ALL_ENVIRONMENTS;
use super::provider::ProviderCache;
use super::refs::SecretRef;
use core::fmt;
use std::collections::BTreeMap;
pub struct SecretView {
environment: String,
store: BTreeMap<String, BTreeMap<String, String>>,
providers: ProviderCache,
}
impl fmt::Debug for SecretView {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SecretView")
.field("environment", &self.environment)
.field("keys", &self.store.len())
.field("namespaces", &self.providers.namespace_count())
.finish()
}
}
pub enum Resolution<'a> {
Found(&'a str),
MissingKey,
MissingNamespace,
}
impl fmt::Debug for Resolution<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Found(_) => "Found(..)",
Self::MissingKey => "MissingKey",
Self::MissingNamespace => "MissingNamespace",
})
}
}
impl SecretView {
#[must_use]
pub fn new(
environment: String,
store: BTreeMap<String, BTreeMap<String, String>>,
providers: ProviderCache,
) -> Self {
Self {
environment,
store,
providers,
}
}
#[must_use]
pub fn empty(environment: String) -> Self {
Self::new(environment, BTreeMap::new(), ProviderCache::default())
}
#[must_use]
pub fn environment(&self) -> &str {
&self.environment
}
#[must_use]
pub fn resolve(&self, reference: &SecretRef<'_>) -> Resolution<'_> {
let table = match reference.namespace {
None => Some(&self.store),
Some(namespace) => self.providers.namespace(namespace),
};
if let Some(value) =
table
.and_then(|table| table.get(reference.key))
.and_then(|by_environment| {
by_environment
.get(&self.environment)
.or_else(|| by_environment.get(ALL_ENVIRONMENTS))
})
{
return Resolution::Found(value.as_str());
}
match reference.namespace {
None => Resolution::MissingKey,
Some(namespace) if self.is_pushed(namespace) => Resolution::MissingKey,
Some(_) => Resolution::MissingNamespace,
}
}
fn is_pushed(&self, namespace: &str) -> bool {
self.providers
.pushed
.get(namespace)
.is_some_and(|environments| {
environments.contains(&self.environment) || environments.contains(ALL_ENVIRONMENTS)
})
}
}
#[cfg(test)]
mod tests {
use super::super::provider::ProviderCache;
use super::super::refs::SecretRef;
use std::collections::{BTreeMap, BTreeSet};
use super::super::testing::*;
use super::*;
#[test]
fn resolution_prefers_the_exact_environment_then_all_then_gives_up() {
let mut store = BTreeMap::new();
store.insert(
"K".to_string(),
BTreeMap::from([
("production".to_string(), "prod".to_string()),
("all".to_string(), "fallback".to_string()),
]),
);
store.insert(
"ONLY_ALL".to_string(),
BTreeMap::from([("all".to_string(), "everywhere".to_string())]),
);
store.insert(
"ONLY_PROD".to_string(),
BTreeMap::from([("production".to_string(), "prod".to_string())]),
);
let view = SecretView::new("staging".to_string(), store, ProviderCache::default());
assert!(matches!(
view.resolve(&SecretRef {
namespace: None,
key: "K"
}),
Resolution::Found("fallback")
));
assert!(matches!(
view.resolve(&SecretRef {
namespace: None,
key: "ONLY_ALL"
}),
Resolution::Found("everywhere")
));
assert!(matches!(
view.resolve(&SecretRef {
namespace: None,
key: "ONLY_PROD"
}),
Resolution::MissingKey
));
assert!(matches!(
view.resolve(&SecretRef {
namespace: None,
key: "ABSENT"
}),
Resolution::MissingKey
));
}
#[test]
fn an_unpopulated_namespace_is_told_apart_from_a_missing_key() {
let view = SecretView::new(
"production".to_string(),
BTreeMap::new(),
vercel_production(),
);
assert!(matches!(
view.resolve(&SecretRef {
namespace: Some("vercel"),
key: "PRESENT"
}),
Resolution::Found("v")
));
assert!(matches!(
view.resolve(&SecretRef {
namespace: Some("vercel"),
key: "ABSENT"
}),
Resolution::MissingKey
));
assert!(matches!(
view.resolve(&SecretRef {
namespace: Some("vault"),
key: "ANY"
}),
Resolution::MissingNamespace
));
}
#[test]
fn a_namespace_pushed_for_another_environment_is_not_populated_for_this_one() {
let view = SecretView::new("staging".to_string(), BTreeMap::new(), vercel_production());
assert!(
matches!(
view.resolve(&SecretRef {
namespace: Some("vercel"),
key: "PRESENT"
}),
Resolution::MissingNamespace
),
"staging has had no push, so waiting is what fixes this"
);
}
#[test]
fn a_pushed_pair_missing_a_key_stays_permanent() {
let view = SecretView::new(
"production".to_string(),
BTreeMap::new(),
vercel_production(),
);
assert!(matches!(
view.resolve(&SecretRef {
namespace: Some("vercel"),
key: "ABSENT"
}),
Resolution::MissingKey
));
}
#[test]
fn an_all_slot_push_makes_a_genuinely_missing_key_permanent() {
let view = SecretView::new("staging".to_string(), BTreeMap::new(), vercel_all());
assert!(matches!(
view.resolve(&SecretRef {
namespace: Some("vercel"),
key: "ABSENT"
}),
Resolution::MissingKey
));
}
#[test]
fn an_all_slot_push_resolves_its_key_for_every_environment() {
let view = SecretView::new("staging".to_string(), BTreeMap::new(), vercel_all());
assert!(matches!(
view.resolve(&SecretRef {
namespace: Some("vercel"),
key: "PRESENT"
}),
Resolution::Found("v")
));
}
#[test]
fn an_empty_push_populates_the_pair_it_carried() {
let view = SecretView::new(
"production".to_string(),
BTreeMap::new(),
ProviderCache::new(
BTreeMap::from([("vercel".to_string(), BTreeMap::new())]),
BTreeMap::from([(
"vercel".to_string(),
BTreeSet::from(["production".to_string()]),
)]),
),
);
assert!(matches!(
view.resolve(&SecretRef {
namespace: Some("vercel"),
key: "ANY"
}),
Resolution::MissingKey
));
}
#[test]
fn debug_never_prints_a_value() {
let store = BTreeMap::from([(
"K".to_string(),
BTreeMap::from([("production".to_string(), "hunter2".to_string())]),
)]);
let view = SecretView::new("production".to_string(), store, ProviderCache::default());
let rendered = format!("{view:?}");
assert_eq!(
rendered,
"SecretView { environment: \"production\", keys: 1, namespaces: 0 }"
);
assert!(!rendered.contains("hunter2"));
}
#[test]
fn a_resolution_debug_never_prints_the_value_it_found() {
assert_eq!(format!("{:?}", Resolution::Found("hunter2")), "Found(..)");
assert_eq!(format!("{:?}", Resolution::MissingKey), "MissingKey");
assert_eq!(
format!("{:?}", Resolution::MissingNamespace),
"MissingNamespace"
);
}
}