#![allow(missing_docs)]
#![allow(unsafe_code)]
#![allow(clippy::needless_pass_by_value)]
use std::sync::Arc;
use rtb_credentials::{
CredentialError, CredentialRef, CredentialStore, EnvStore, ExposeSecret, KeychainRef,
KeyringStore, LiteralStore, MemoryStore, Resolver, SecretString,
};
fn s(v: &str) -> SecretString {
SecretString::from(v.to_string())
}
#[test]
fn t1_store_is_object_safe() {
let _erased: Arc<dyn CredentialStore> = Arc::new(MemoryStore::new());
}
#[tokio::test]
async fn t2_memory_roundtrip() {
let store = MemoryStore::new();
store.set("svc", "acct", s("hunter2")).await.unwrap();
let got = store.get("svc", "acct").await.unwrap();
assert_eq!(got.expose_secret(), "hunter2");
store.delete("svc", "acct").await.unwrap();
match store.get("svc", "acct").await {
Err(CredentialError::NotFound { .. }) => {}
other => panic!("expected NotFound after delete, got {other:?}"),
}
}
#[tokio::test]
async fn t3_memory_not_found() {
let store = MemoryStore::new();
match store.get("svc", "missing").await {
Err(CredentialError::NotFound { name }) => assert_eq!(name, "svc/missing"),
other => panic!("expected NotFound, got {other:?}"),
}
}
#[tokio::test]
async fn t4_env_store_reads_env() {
unsafe {
std::env::set_var("RTBCRED_T4_VAR", "env-value");
}
let store = EnvStore::new();
let got = store.get("unused", "RTBCRED_T4_VAR").await.unwrap();
assert_eq!(got.expose_secret(), "env-value");
unsafe {
std::env::remove_var("RTBCRED_T4_VAR");
}
}
#[tokio::test]
async fn t5_env_store_not_found() {
unsafe {
std::env::remove_var("RTBCRED_T5_DEFINITELY_MISSING");
}
let store = EnvStore::new();
match store.get("", "RTBCRED_T5_DEFINITELY_MISSING").await {
Err(CredentialError::NotFound { name }) => {
assert_eq!(name, "RTBCRED_T5_DEFINITELY_MISSING");
}
other => panic!("expected NotFound, got {other:?}"),
}
}
#[tokio::test]
async fn t6_literal_store_returns_constant() {
let store = LiteralStore::new(s("baked-in"));
let got = store.get("anything", "anywhere").await.unwrap();
assert_eq!(got.expose_secret(), "baked-in");
}
#[tokio::test]
async fn t7_literal_store_is_read_only() {
let store = LiteralStore::new(s("x"));
assert!(matches!(store.set("a", "b", s("y")).await, Err(CredentialError::ReadOnly),));
assert!(matches!(store.delete("a", "b").await, Err(CredentialError::ReadOnly)));
}
#[tokio::test]
async fn t8_resolver_precedence() {
unsafe {
std::env::set_var("RTBCRED_T8_ENV", "env-wins");
std::env::set_var("RTBCRED_T8_FALLBACK", "fallback-wins");
std::env::remove_var("CI");
}
let store = Arc::new(MemoryStore::new());
store.set("t8svc", "t8acct", s("keychain-wins")).await.unwrap();
let resolver = Resolver::new(store.clone());
let all_set = CredentialRef {
env: Some("RTBCRED_T8_ENV".into()),
keychain: Some(KeychainRef { service: "t8svc".into(), account: "t8acct".into() }),
literal: Some(s("literal-wins")),
fallback_env: Some("RTBCRED_T8_FALLBACK".into()),
};
assert_eq!(resolver.resolve(&all_set).await.unwrap().expose_secret(), "env-wins");
let without_env = CredentialRef { env: None, ..all_set.clone() };
assert_eq!(resolver.resolve(&without_env).await.unwrap().expose_secret(), "keychain-wins",);
let without_keychain = CredentialRef { keychain: None, ..without_env };
assert_eq!(resolver.resolve(&without_keychain).await.unwrap().expose_secret(), "literal-wins",);
let without_literal = CredentialRef { literal: None, ..without_keychain };
assert_eq!(resolver.resolve(&without_literal).await.unwrap().expose_secret(), "fallback-wins",);
unsafe {
std::env::remove_var("RTBCRED_T8_ENV");
std::env::remove_var("RTBCRED_T8_FALLBACK");
}
}
#[tokio::test]
async fn t9_literal_refused_in_ci() {
let prior = std::env::var("CI").ok();
unsafe {
std::env::set_var("CI", "true");
}
let store = Arc::new(MemoryStore::new());
let resolver = Resolver::new(store);
let cref = CredentialRef { literal: Some(s("would-leak-to-ci")), ..CredentialRef::default() };
match resolver.resolve(&cref).await {
Err(CredentialError::LiteralRefusedInCi) => {}
other => panic!("expected LiteralRefusedInCi, got {other:?}"),
}
unsafe {
match prior {
Some(v) => std::env::set_var("CI", v),
None => std::env::remove_var("CI"),
}
}
}
#[tokio::test]
async fn t10_resolver_empty_ref_not_found() {
let store = Arc::new(MemoryStore::new());
let resolver = Resolver::new(store);
match resolver.resolve(&CredentialRef::default()).await {
Err(CredentialError::NotFound { name }) => {
assert_eq!(name, "<unnamed credential>");
}
other => panic!("expected NotFound, got {other:?}"),
}
}
#[test]
fn t11_debug_redacted() {
let secret = s("super-secret-value-123");
let debug = format!("{secret:?}");
assert!(!debug.contains("super-secret-value-123"), "Debug leaked the secret: {debug}");
}
#[tokio::test]
async fn t12_keyring_store_missing_is_not_found() {
let store = KeyringStore::new();
match store.get("rtb-credentials-smoke", "definitely-not-present").await {
Err(CredentialError::NotFound { .. } | CredentialError::Keychain(_)) => {}
other => panic!("expected NotFound or Keychain, got {other:?}"),
}
}
#[test]
fn t13_resolver_with_platform_default_builds() {
let _ = rtb_credentials::Resolver::with_platform_default();
let _: rtb_credentials::Resolver = rtb_credentials::Resolver::default();
}