Test code that resolves credentials¶
MemoryStore is exported from the crate root, not hidden behind a
test feature, so downstream test suites can use it directly.
Inject a MemoryStore instead of the platform keychain¶
use rtb_credentials::{CredentialStore, MemoryStore, Resolver, SecretString};
use std::sync::Arc;
#[tokio::test]
async fn resolves_from_the_keychain_leg() {
let store = Arc::new(MemoryStore::new());
store
.set("mytool", "anthropic", SecretString::from("test-secret".to_string()))
.await
.unwrap();
let resolver = Resolver::new(store);
// ...
}
Write production code that takes Arc<dyn CredentialStore> rather than
calling Resolver::with_platform_default() internally. That one choice
is what makes the code testable — with_platform_default hard-wires
KeyringStore and cannot be substituted.
Assert on the secret with expose_secret¶
use rtb_credentials::ExposeSecret;
let secret = resolver.resolve(&cref).await.unwrap();
assert_eq!(secret.expose_secret(), "test-secret");
SecretString does not implement PartialEq, so comparing two
SecretStrings directly does not compile. Compare the exposed &str.
Build a CredentialRef without deserialising config¶
All four fields are public, and Default supplies an empty reference:
use rtb_credentials::{CredentialRef, KeychainRef};
let cref = CredentialRef {
keychain: Some(KeychainRef {
service: "mytool".to_string(),
account: "anthropic".to_string(),
}),
..CredentialRef::default()
};
Avoid environment variables in parallel tests¶
Rust runs tests in threads within one process, so set_var and
remove_var are shared mutable state across every test in the binary. A
test that sets CI, or a variable another test reads, will
intermittently corrupt its neighbours.
Two rules keep this manageable:
Use a unique variable name per test. The crate's own suite does
exactly this — RTBCRED_T4_VAR, RTBCRED_T8_ENV — so no two tests
contend for the same name.
Prefer the keychain leg. A MemoryStore is per-test state with no
global reach, so a test exercising resolution through the keychain leg
needs no environment mutation at all. Reach for env vars only when the
env legs are what you are testing.
CI is the one name that cannot be made unique, since the crate reads
that specific variable. A test that sets it must restore the previous
value:
let prior = std::env::var("CI").ok();
// ... set CI, run the assertion ...
match prior {
Some(v) => unsafe { std::env::set_var("CI", v) },
None => unsafe { std::env::remove_var("CI") },
}
Even restored, such a test races with any concurrent test that resolves a literal. Consider keeping the CI-refusal test in its own test binary.
Do not use LiteralStore as a test keychain¶
LiteralStore::get returns its one secret for every service and
account, so a test using it cannot detect a lookup with the wrong
identifiers — every key appears to exist.
MemoryStore returns NotFound for keys that were not set, which is
what makes a negative assertion meaningful.
Assert on the failure paths¶
The behaviours most likely to surprise a user are the ones worth pinning:
use rtb_credentials::CredentialError;
// nothing configured
let empty = CredentialRef::default();
assert!(matches!(
resolver.resolve(&empty).await,
Err(CredentialError::NotFound { .. })
));
A store returning a Keychain error rather than NotFound aborts the
whole chain, so a test that a fallback takes over should use a store
returning NotFound — which MemoryStore does for any key it was not
given.