use super::super::*;
fn temp_store() -> (tempfile::TempDir, std::path::PathBuf) {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("drep").join("auth.toml");
(dir, path)
}
#[test]
fn a_missing_file_loads_as_an_empty_store() {
let (_dir, path) = temp_store();
let store = AuthStore::load(&path).expect("a missing store is not an error");
assert!(store.is_empty());
assert_eq!(store.endpoints(), Vec::<&str>::new());
}
#[test]
fn a_stored_key_round_trips_through_the_file() {
let (_dir, path) = temp_store();
let mut store = AuthStore::new();
store
.set("https://api.kimi.com/coding/v1", "k-1")
.expect("set");
store.save(&path).expect("save");
let reloaded = AuthStore::load(&path).expect("load");
assert_eq!(reloaded.get("https://api.kimi.com/coding/v1"), Some("k-1"));
}
#[test]
fn a_corrupt_store_is_an_error_rather_than_an_empty_one() {
let (_dir, path) = temp_store();
std::fs::create_dir_all(path.parent().expect("parent")).expect("mkdir");
std::fs::write(&path, "this is not toml {{{").expect("write");
let err = AuthStore::load(&path).expect_err("a corrupt store is fatal");
assert!(
matches!(err, AuthError::Parse(..)),
"expected a parse error, got {err:?}"
);
}
#[test]
fn saving_creates_the_directory() {
let (_dir, path) = temp_store();
let mut store = AuthStore::new();
store.set("https://e/v1", "k").expect("set");
store.save(&path).expect("save creates its parent");
assert!(path.exists());
}
#[cfg(unix)]
#[test]
fn the_saved_file_is_readable_only_by_its_owner() {
use std::os::unix::fs::PermissionsExt;
let (_dir, path) = temp_store();
let mut store = AuthStore::new();
store.set("https://e/v1", "k").expect("set");
store.save(&path).expect("save");
let mode = std::fs::metadata(&path)
.expect("metadata")
.permissions()
.mode();
assert_eq!(mode & 0o777, 0o600, "got {:o}", mode & 0o777);
}
#[cfg(unix)]
#[test]
fn the_saved_directory_is_enterable_only_by_its_owner() {
use std::os::unix::fs::PermissionsExt;
let (_dir, path) = temp_store();
let mut store = AuthStore::new();
store.set("https://e/v1", "k").expect("set");
store.save(&path).expect("save");
let parent = path.parent().expect("parent");
let mode = std::fs::metadata(parent)
.expect("metadata")
.permissions()
.mode();
assert_eq!(mode & 0o777, 0o700, "got {:o}", mode & 0o777);
}
#[cfg(unix)]
#[test]
fn saving_does_not_narrow_an_existing_directory() {
use std::os::unix::fs::PermissionsExt;
let (_dir, path) = temp_store();
let parent = path.parent().expect("parent");
std::fs::create_dir_all(parent).expect("mkdir");
std::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o755)).expect("widen");
let mut store = AuthStore::new();
store.set("https://e/v1", "k").expect("set");
store.save(&path).expect("save");
let mode = std::fs::metadata(parent)
.expect("metadata")
.permissions()
.mode();
assert_eq!(mode & 0o777, 0o755, "got {:o}", mode & 0o777);
}
#[cfg(unix)]
#[test]
fn saving_narrows_a_file_whose_mode_was_widened() {
use std::os::unix::fs::PermissionsExt;
let (_dir, path) = temp_store();
let mut store = AuthStore::new();
store.set("https://e/v1", "k").expect("set");
store.save(&path).expect("first save");
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644)).expect("widen");
store.save(&path).expect("second save");
let mode = std::fs::metadata(&path)
.expect("metadata")
.permissions()
.mode();
assert_eq!(mode & 0o777, 0o600, "got {:o}", mode & 0o777);
}
#[test]
fn an_endpoint_is_matched_regardless_of_a_trailing_slash_or_case() {
let mut store = AuthStore::new();
store
.set("https://API.Z.AI/api/coding/paas/v4/", "k")
.expect("set");
assert_eq!(store.get("https://api.z.ai/api/coding/paas/v4"), Some("k"));
assert_eq!(store.get("https://api.z.ai/api/coding/paas/v4/"), Some("k"));
assert_eq!(store.get("https://API.Z.AI/api/coding/paas/v4"), Some("k"));
}
#[test]
fn two_paths_on_one_host_are_separate_credentials() {
let mut store = AuthStore::new();
store
.set("https://api.minimax.io/v1", "openai-key")
.expect("set");
store
.set("https://api.minimax.io/anthropic/v1", "anthropic-key")
.expect("set");
assert_eq!(store.get("https://api.minimax.io/v1"), Some("openai-key"));
assert_eq!(
store.get("https://api.minimax.io/anthropic/v1"),
Some("anthropic-key")
);
}
#[test]
fn setting_the_same_endpoint_twice_replaces_the_key() {
let mut store = AuthStore::new();
store.set("https://e/v1", "old").expect("set");
store.set("https://e/v1", "new").expect("set");
assert_eq!(store.get("https://e/v1"), Some("new"));
assert_eq!(store.endpoints().len(), 1, "not a second entry");
}
#[test]
fn a_pasted_key_loses_its_surrounding_whitespace() {
let mut store = AuthStore::new();
store.set("https://e/v1", " sk-abc\n").expect("set");
assert_eq!(store.get("https://e/v1"), Some("sk-abc"));
}
#[test]
fn an_empty_key_is_refused_rather_than_stored() {
let mut store = AuthStore::new();
let err = store
.set("https://e/v1", " ")
.expect_err("empty is refused");
assert!(
matches!(err, AuthError::EmptyKey(ref e) if e == "https://e/v1"),
"expected EmptyKey naming the endpoint, got {err:?}"
);
assert!(store.is_empty(), "and nothing was stored");
}
#[test]
fn removing_reports_whether_anything_was_held() {
let mut store = AuthStore::new();
store.set("https://e/v1", "k").expect("set");
assert!(store.remove("https://e/v1"), "held a key");
assert!(!store.remove("https://e/v1"), "no longer holds one");
assert!(store.is_empty());
}
#[test]
fn removing_normalises_the_endpoint_too() {
let mut store = AuthStore::new();
store.set("https://e/v1", "k").expect("set");
assert!(
store.remove("https://E/v1/"),
"the same endpoint, spelled differently"
);
}
#[test]
fn endpoints_are_listed_in_a_stable_order() {
let mut store = AuthStore::new();
for endpoint in ["https://c/v1", "https://a/v1", "https://b/v1"] {
store.set(endpoint, "k").expect("set");
}
assert_eq!(
store.endpoints(),
vec!["https://a/v1", "https://b/v1", "https://c/v1"]
);
}
#[test]
fn debug_prints_endpoints_and_never_keys() {
let mut store = AuthStore::new();
store.set("https://e/v1", "sk-super-secret").expect("set");
let rendered = format!("{store:?}");
assert!(rendered.contains("https://e/v1"), "got {rendered}");
assert!(
!rendered.contains("sk-super-secret"),
"the key reached a log: {rendered}"
);
}
#[test]
fn the_default_path_the_override_and_a_round_trip_through_it() {
let platform = path_from(None).expect("this platform has a config dir");
assert_eq!(platform.file_name().expect("file name"), "auth.toml");
assert!(
platform.to_string_lossy().contains("drep"),
"sits under an application-identity directory: {}",
platform.display()
);
let dir = tempfile::tempdir().expect("tempdir");
let scratch = dir.path().join("nested").join("auth.toml");
assert_eq!(
path_from(Some(scratch.clone().into_os_string())).expect("path"),
scratch,
"the override wins over the platform path"
);
let mut store = AuthStore::new();
store.set("https://e/v1", "k-default").expect("set");
store.save(&scratch).expect("save writes to the override");
assert!(scratch.exists(), "a file was actually written");
assert_eq!(
AuthStore::load(&scratch)
.expect("reload")
.get("https://e/v1"),
Some("k-default"),
"and it round-trips"
);
}
#[test]
fn a_key_carrying_a_control_character_is_refused() {
let mut store = AuthStore::new();
let err = store
.set("https://e/v1", "sk-abc\u{1b}[0mdef")
.expect_err("an escape sequence is not a usable key");
assert!(matches!(err, AuthError::UnusableKey(_)), "got {err:?}");
assert!(store.is_empty(), "and nothing was stored");
}
#[test]
fn an_interior_newline_is_refused_but_a_trailing_one_is_trimmed() {
let mut store = AuthStore::new();
store
.set("https://e/v1", "sk-fine\n")
.expect("trailing is trimmed");
assert_eq!(store.get("https://e/v1"), Some("sk-fine"));
assert!(
store.set("https://e/v1", "sk-bro\nken").is_err(),
"an interior newline cannot be sent"
);
}
#[test]
fn a_url_path_keeps_its_case_while_the_host_does_not() {
let mut store = AuthStore::new();
store
.set("https://Example.COM/API/v1", "upper")
.expect("set");
store
.set("https://example.com/api/v1", "lower")
.expect("set");
assert_eq!(
store.endpoints().len(),
2,
"two distinct paths, two entries"
);
assert_eq!(store.get("https://EXAMPLE.com/API/v1"), Some("upper"));
assert_eq!(store.get("https://example.com/api/v1"), Some("lower"));
}
#[test]
fn saving_leaves_no_temporary_beside_the_store() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("auth.toml");
let mut store = AuthStore::new();
store.set("https://api.z.ai/v1", "sk-one").expect("set");
store.save(&path).expect("first save");
store.set("https://api.z.ai/v1", "sk-two").expect("set");
store.save(&path).expect("second save");
let names: Vec<String> = std::fs::read_dir(dir.path())
.expect("read_dir")
.map(|entry| {
entry
.expect("entry")
.file_name()
.to_string_lossy()
.into_owned()
})
.collect();
assert_eq!(names, vec!["auth.toml".to_string()], "got {names:?}");
assert_eq!(
AuthStore::load(&path)
.expect("loads")
.get("https://api.z.ai/v1"),
Some("sk-two"),
"and the second save is the one that survived"
);
}
#[cfg(unix)]
#[test]
fn a_preexisting_temporary_symlink_cannot_receive_credentials() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("auth.toml");
let victim = dir.path().join("unrelated");
std::fs::write(&victim, "leave me alone").expect("victim");
std::os::unix::fs::symlink(&victim, path.with_file_name("auth.toml.drep-tmp"))
.expect("plant predictable temporary symlink");
let mut store = AuthStore::new();
store
.set("https://api.z.ai/v1", "sk-must-not-leak")
.expect("set");
store.save(&path).expect("save through a fresh temporary");
assert_eq!(
std::fs::read_to_string(&victim).expect("victim remains readable"),
"leave me alone",
"the credential writer must not follow a planted temporary symlink"
);
assert_eq!(
AuthStore::load(&path)
.expect("load")
.get("https://api.z.ai/v1"),
Some("sk-must-not-leak")
);
}
#[test]
fn the_private_temporary_is_created_beside_the_store() {
let nested = Path::new("configured/drep/auth.toml");
assert_eq!(temporary_parent(nested), Path::new("configured/drep"));
assert_eq!(temporary_parent(Path::new("auth.toml")), Path::new("."));
}
#[test]
fn a_save_that_cannot_be_published_leaves_the_previous_store_readable() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("auth.toml");
let mut store = AuthStore::new();
store
.set("https://api.z.ai/v1", "sk-original")
.expect("set");
store.save(&path).expect("the first save succeeds");
std::fs::remove_file(&path).expect("remove");
std::fs::create_dir(&path).expect("mkdir");
std::fs::write(path.join("occupied"), "x").expect("write");
let mut replacement = AuthStore::new();
replacement
.set("https://api.z.ai/v1", "sk-replacement")
.expect("set");
assert!(
replacement.save(&path).is_err(),
"publishing over a non-empty directory cannot succeed"
);
assert!(
path.join("occupied").exists(),
"and what was there is untouched"
);
}
#[test]
fn a_scheme_less_endpoints_path_is_case_sensitive_too() {
let mut store = AuthStore::new();
store.set("LocalHost:11434/V1", "sk-upper").expect("set");
store.set("localhost:11434/v1", "sk-lower").expect("set");
assert_eq!(store.get("localhost:11434/V1"), Some("sk-upper"));
assert_eq!(store.get("LOCALHOST:11434/v1"), Some("sk-lower"));
}