Skip to content

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.