use brazen::{AmbientFormat, AmbientSpec, Cred, CredStore, Secret};
use super::creds::expand_home_with;
use super::XdgCredStore;
fn store_at(dir: std::path::PathBuf) -> XdgCredStore {
XdgCredStore { dir: Some(dir) }
}
fn oauth_cred(access: &str, expires_at: u64) -> Cred {
Cred::OAuth2 {
access_token: Secret::new(access),
refresh_token: Secret::new("refresh-tok"),
expires_at,
scope: Some("openid profile".to_owned()),
account_id: Some("acct-1".to_owned()),
}
}
#[test]
fn get_after_put_roundtrips_oauth2() {
let tmp = tempfile::tempdir().unwrap();
let store = store_at(tmp.path().join("credentials"));
assert_eq!(
store.get("anthropic"),
None,
"a miss before any write is None"
);
let cred = oauth_cred("access-tok", 1_750_000_000);
store.put("anthropic", &cred).unwrap();
assert_eq!(
store.get("anthropic"),
Some(cred),
"get must round-trip the persisted Cred::OAuth2 byte-for-byte"
);
assert_eq!(
store.get("openai"),
None,
"an unwritten provider is still a miss, not a cross-read"
);
}
#[cfg(unix)]
#[test]
fn written_file_is_0600_and_dir_is_0700() {
use std::os::unix::fs::PermissionsExt;
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path().join("credentials");
let store = store_at(dir.clone());
store.put("anthropic", &oauth_cred("a", 1)).unwrap();
let file_mode = std::fs::metadata(dir.join("anthropic.json"))
.unwrap()
.permissions()
.mode();
assert_eq!(file_mode & 0o777, 0o600, "the cred file must be owner-only");
let dir_mode = std::fs::metadata(&dir).unwrap().permissions().mode();
assert_eq!(dir_mode & 0o777, 0o700, "the cred dir must be owner-only");
}
#[cfg(unix)]
#[test]
fn concurrent_reads_never_observe_a_partial_write() {
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread;
let tmp = tempfile::tempdir().unwrap();
let store = Arc::new(store_at(tmp.path().join("credentials")));
let a = oauth_cred("token-aaaaaaaaaaaaaaaaaaaa", 100);
let b = oauth_cred("token-bbbbbbbbbbbbbbbbbbbb", 200);
store.put("anthropic", &a).unwrap();
let done = Arc::new(AtomicBool::new(false));
let readers: Vec<_> = (0..3)
.map(|_| {
let (store, done, a, b) = (store.clone(), done.clone(), a.clone(), b.clone());
thread::spawn(move || {
let mut reads = 0u64;
while !done.load(Ordering::Relaxed) {
let got = store.get("anthropic");
assert!(
got.as_ref() == Some(&a) || got.as_ref() == Some(&b),
"reader observed a torn/partial cred: {got:?}"
);
reads += 1;
}
reads
})
})
.collect();
for i in 0..3000u64 {
store
.put("anthropic", if i % 2 == 0 { &a } else { &b })
.unwrap();
}
done.store(true, Ordering::Relaxed);
let total: u64 = readers.into_iter().map(|h| h.join().unwrap()).sum();
assert!(total > 0, "the readers must have raced at least one write");
}
const CLAUDE_CODE: &str = r#"{"claudeAiOauth":{"accessToken":"at-cc","refreshToken":"rt-cc","expiresAt":1781693903571,"scopes":["user:inference"]}}"#;
#[test]
fn discover_reads_and_parses_an_ambient_file() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("cc.json");
std::fs::write(&path, CLAUDE_CODE).unwrap();
let store = store_at(tmp.path().join("credentials"));
let spec = AmbientSpec {
format: AmbientFormat::ClaudeCode,
path: path.to_string_lossy().into_owned(),
};
match store.discover(&spec) {
Some(Cred::OAuth2 {
access_token,
expires_at,
..
}) => {
assert_eq!(access_token.expose(), "at-cc");
assert_eq!(expires_at, 1_781_693_903);
}
other => panic!("expected a discovered OAuth2 cred, got {other:?}"),
}
}
#[test]
fn discover_is_none_for_missing_or_malformed_files() {
let tmp = tempfile::tempdir().unwrap();
let store = store_at(tmp.path().join("credentials"));
let missing = AmbientSpec {
format: AmbientFormat::ClaudeCode,
path: tmp.path().join("nope.json").to_string_lossy().into_owned(),
};
assert_eq!(
store.discover(&missing),
None,
"absent file is the no-creds path"
);
let bad = tmp.path().join("bad.json");
std::fs::write(&bad, "not json").unwrap();
let bad_spec = AmbientSpec {
format: AmbientFormat::ClaudeCode,
path: bad.to_string_lossy().into_owned(),
};
assert_eq!(
store.discover(&bad_spec),
None,
"foreign/garbage file is None"
);
}
#[test]
fn expand_home_substitutes_leading_tilde_and_passes_others_through() {
let home = tempfile::tempdir().unwrap();
let home_path = home.path().to_path_buf();
let some_home = || Some(home_path.clone().into_os_string());
assert_eq!(
expand_home_with("~/.claude/.credentials.json", some_home()),
Some(home_path.join(".claude/.credentials.json")),
);
assert_eq!(
expand_home_with("/etc/creds.json", some_home()),
Some(std::path::PathBuf::from("/etc/creds.json")),
);
assert_eq!(
expand_home_with("~/x", None),
None,
"no home ⇒ no expansion"
);
}