use std::collections::HashMap;
use std::sync::{Arc, Once};
use crate::Store;
use crate::cred::Cred;
use crate::utils::{validate_attributes, validate_target};
use keyring_core::{CredentialStore, Entry, Error, api::CredentialPersistence, get_default_store};
use windows_sys::Win32::Security::Credentials::{
CRED_MAX_GENERIC_TARGET_NAME_LENGTH, CRED_MAX_STRING_LENGTH, CRED_MAX_USERNAME_LENGTH,
};
static SET_STORE: Once = Once::new();
fn usually_goes_in_main() {
keyring_core::set_default_store(Store::new().unwrap());
}
#[test]
fn test_store_methods() {
SET_STORE.call_once(usually_goes_in_main);
let store = get_default_store().unwrap();
let vendor1 = store.vendor();
let id1 = store.id();
let vendor2 = store.vendor();
let id2 = store.id();
assert_eq!(vendor1, vendor2);
assert_eq!(id1, id2);
let store2: Arc<CredentialStore> = Store::new().unwrap();
let vendor3 = store2.vendor();
let id3 = store2.id();
assert_eq!(vendor1, vendor3);
assert_ne!(id1, id3);
}
fn entry_new(service: &str, user: &str) -> Entry {
SET_STORE.call_once(usually_goes_in_main);
Entry::new(&format!("test-{service}"), user).unwrap_or_else(|err| {
panic!("Couldn't create entry (service: {service}, user: {user}): {err:?}")
})
}
fn entry_new_with_modifiers(service: &str, user: &str, modifiers: &HashMap<&str, &str>) -> Entry {
SET_STORE.call_once(usually_goes_in_main);
Entry::new_with_modifiers(&format!("test-{service}"), user, modifiers).unwrap_or_else(|err| {
panic!("Couldn't create entry (service: {service}, user: {user}, modifiers: {modifiers:?}): {err:?})")
})
}
fn generate_random_string_of_len(len: usize) -> String {
use fastrand;
use std::iter::repeat_with;
repeat_with(fastrand::alphanumeric).take(len).collect()
}
fn generate_random_string() -> String {
generate_random_string_of_len(12usize)
}
fn generate_random_bytes() -> Vec<u8> {
use fastrand;
use std::iter::repeat_with;
repeat_with(|| fastrand::u8(..)).take(24).collect()
}
fn test_round_trip_no_delete(case: &str, entry: &Entry, in_pass: &str) {
entry
.set_password(in_pass)
.unwrap_or_else(|err| panic!("Can't set password for {case}: {err:?}"));
let out_pass = entry
.get_password()
.unwrap_or_else(|err| panic!("Can't get password: {case}: {err:?}"));
assert_eq!(
in_pass, out_pass,
"Passwords don't match for {case}: set='{in_pass}', get='{out_pass}'",
)
}
fn test_round_trip(case: &str, entry: &Entry, in_pass: &str) {
test_round_trip_no_delete(case, entry, in_pass);
entry
.delete_credential()
.unwrap_or_else(|err| panic!("Can't delete password: {case}: {err:?}"));
let password = entry.get_password();
assert!(
matches!(password, Err(Error::NoEntry)),
"Got a deleted password: {case}",
);
}
pub fn test_round_trip_secret(case: &str, entry: &Entry, in_secret: &[u8]) {
entry
.set_secret(in_secret)
.unwrap_or_else(|err| panic!("Can't set secret for {case}: {err:?}"));
let out_secret = entry
.get_secret()
.unwrap_or_else(|err| panic!("Can't get secret for {case}: {err:?}"));
assert_eq!(
in_secret, &out_secret,
"Secrets don't match for {case}: set='{in_secret:?}', get='{out_secret:?}'",
);
entry
.delete_credential()
.unwrap_or_else(|err| panic!("Can't delete credential for {case}: {err:?}"));
let secret = entry.get_secret();
assert!(
matches!(secret, Err(Error::NoEntry)),
"Got a deleted password: {case}",
);
}
#[test]
fn test_validate() {
validate_target("target", "user").unwrap();
validate_target("", "user").unwrap_err();
validate_target(
&generate_random_string_of_len(CRED_MAX_GENERIC_TARGET_NAME_LENGTH as usize + 1),
"user",
)
.unwrap_err();
validate_target(
"target",
&generate_random_string_of_len(CRED_MAX_USERNAME_LENGTH as usize + 1),
)
.unwrap_err();
validate_attributes("username", "target_alias", "comment").unwrap();
validate_attributes(
&generate_random_string_of_len(CRED_MAX_USERNAME_LENGTH as usize + 1),
"target_alias",
"comment",
)
.unwrap_err();
validate_attributes(
"username",
"target_alias",
&generate_random_string_of_len(CRED_MAX_STRING_LENGTH as usize + 1),
)
.unwrap_err();
}
#[test]
fn test_invalid_parameter() {
SET_STORE.call_once(usually_goes_in_main);
let modifiers = HashMap::from([("target", "")]);
let entry = Entry::new_with_modifiers("service", "user", &modifiers);
assert!(matches!(entry, Err(Error::Invalid(_, _))));
let entry =
Entry::new_with_modifiers("service", "user", &HashMap::from([("persistence", "none")]));
assert!(matches!(entry, Err(Error::Invalid(_, _))));
let store: Arc<CredentialStore> =
Store::new_with_configuration(&HashMap::from([("service_no_divider", "true")])).unwrap();
let entry = store.build("ser.vice", "user", None);
assert!(matches!(entry, Err(Error::Invalid(_, _))));
let store: Arc<CredentialStore> = Store::new_with_configuration(&HashMap::from([
("service_no_divider", "true"),
("divider", ""),
]))
.unwrap();
let entry = store.build("service", "user", None);
assert!(matches!(entry, Err(Error::Invalid(_, _))));
}
#[test]
fn test_missing_entry() {
let name = generate_random_string();
let entry = entry_new(&name, &name);
assert!(matches!(entry.get_password(), Err(Error::NoEntry)))
}
#[test]
fn test_empty_password() {
let name = generate_random_string();
let in_pass = "";
let entry = entry_new(&name, &name);
entry.set_password(in_pass).unwrap();
assert_eq!(entry.get_password().unwrap(), in_pass);
_ = entry.delete_credential();
}
#[test]
fn test_round_trip_ascii_password() {
let name = generate_random_string();
let entry = entry_new(&name, &name);
test_round_trip("ascii password", &entry, "test ascii password");
}
#[test]
fn test_round_trip_non_ascii_password() {
let name = generate_random_string();
let entry = entry_new(&name, &name);
test_round_trip("non-ascii password", &entry, "このきれいな花は桜です");
}
#[test]
fn test_entries_with_same_and_different_specifiers() {
let name1 = generate_random_string();
let name2 = generate_random_string();
let entry1 = entry_new(&name1, &name2);
let entry2 = entry_new(&name1, &name2);
let entry3 = entry_new(&name2, &name1);
entry1.set_password("test password").unwrap();
let pw2 = entry2.get_password().unwrap();
assert_eq!(pw2, "test password");
_ = entry3.get_password().unwrap_err();
entry1.delete_credential().unwrap();
_ = entry2.get_password().unwrap_err();
entry3.delete_credential().unwrap_err();
}
#[test]
fn test_round_trip_random_secret() {
let name = generate_random_string();
let entry = entry_new(&name, &name);
let secret = generate_random_bytes();
test_round_trip_secret("non-ascii password", &entry, secret.as_slice());
}
#[test]
fn test_update() {
let name = generate_random_string();
let entry = entry_new(&name, &name);
test_round_trip_no_delete("initial ascii password", &entry, "test ascii password");
test_round_trip(
"updated non-ascii password",
&entry,
"このきれいな花は桜です",
);
}
#[test]
fn test_get_update_attributes() {
let name = generate_random_string();
let entry = entry_new(&name, &name);
entry.set_password("test password").unwrap();
let before = entry.get_attributes().unwrap();
assert_eq!(before["username"], name);
assert_eq!(before["target_alias"], "");
assert_eq!(before["comment"], "");
let in_map: HashMap<&str, &str> = HashMap::from([
("target_alias", "target alias value"),
("comment", "comment value"),
("username", "username value"),
]);
entry.update_attributes(&in_map).unwrap();
let after = entry.get_attributes().unwrap();
assert_eq!(after["username"], "username value");
assert_eq!(after["target_alias"], "target alias value");
assert_eq!(after["comment"], "comment value");
entry.delete_credential().unwrap();
}
#[test]
fn test_get_credential_and_specifiers() {
let name1 = generate_random_string();
let name2 = generate_random_string();
let entry1 = entry_new(&name1, &name2);
assert!(matches!(entry1.get_credential(), Err(Error::NoEntry)));
entry1.set_password("password for entry1").unwrap();
let cred1 = entry1.as_any().downcast_ref::<Cred>().unwrap();
let wrapper = entry1.get_credential().unwrap();
let cred2 = wrapper.as_any().downcast_ref::<Cred>().unwrap();
assert_eq!(cred1 as *const _, cred2 as *const _);
let (service, user) = wrapper.get_specifiers().unwrap();
assert_eq!(service, format!("test-{name1}"));
assert_eq!(user, name2);
entry1.delete_credential().unwrap();
wrapper.delete_credential().unwrap_err();
let modifiers = HashMap::from([("target", name1.as_str())]);
let entry2 = Entry::new_with_modifiers(&name1, &name2, &modifiers).unwrap();
assert!(entry2.get_specifiers().is_none());
entry2.delete_credential().unwrap_err();
}
#[test]
fn test_create_then_move() {
let name = generate_random_string();
let entry = entry_new(&name, &name);
let test = move || {
let password = "test ascii password";
entry.set_password(password).unwrap();
let stored_password = entry.get_password().unwrap();
assert_eq!(stored_password, password);
let password = "このきれいな花は桜です";
entry.set_password(password).unwrap();
let stored_password = entry.get_password().unwrap();
assert_eq!(stored_password, password);
entry.delete_credential().unwrap();
assert!(matches!(entry.get_password(), Err(Error::NoEntry)));
};
let handle = std::thread::spawn(test);
assert!(handle.join().is_ok(), "Couldn't execute on thread")
}
#[test]
fn test_simultaneous_create_then_move() {
let mut handles = vec![];
let base = generate_random_string();
for i in 0..10 {
let name = format!("{base}-{i}");
let entry = entry_new(&name, &name);
let test = move || {
entry.set_password(&name).unwrap();
let stored_password = entry.get_password().unwrap();
assert_eq!(stored_password, name);
entry.delete_credential().unwrap();
assert!(matches!(entry.get_password(), Err(Error::NoEntry)));
};
handles.push(std::thread::spawn(test))
}
for handle in handles {
handle.join().unwrap()
}
}
#[test]
fn test_create_set_then_move() {
let name = generate_random_string();
let entry = entry_new(&name, &name);
let password = "test ascii password";
entry.set_password(password).unwrap();
let test = move || {
let stored_password = entry.get_password().unwrap();
assert_eq!(stored_password, password);
entry.delete_credential().unwrap();
assert!(matches!(entry.get_password(), Err(Error::NoEntry)));
};
let handle = std::thread::spawn(test);
assert!(handle.join().is_ok(), "Couldn't execute on thread")
}
#[test]
#[ignore] fn test_simultaneous_create_set_then_move() {
let mut handles = vec![];
let base = generate_random_string();
for i in 0..10 {
let name = format!("{base}-{i}");
let entry = entry_new(&name, &name);
entry.set_password(&name).unwrap();
let test = move || {
let stored_password = entry.get_password().unwrap();
assert_eq!(stored_password, name);
entry.delete_credential().unwrap();
assert!(matches!(entry.get_password(), Err(Error::NoEntry)));
};
handles.push(std::thread::spawn(test))
}
for handle in handles {
handle.join().unwrap()
}
}
#[test]
fn test_simultaneous_independent_create_set() {
let mut handles = vec![];
let base = generate_random_string();
for i in 0..10 {
let name = format!("{base}-{i}");
let test = move || {
let entry = entry_new(&name, &name);
entry.set_password(&name).unwrap();
let stored_password = entry.get_password().unwrap();
assert_eq!(stored_password, name);
entry.delete_credential().unwrap();
assert!(matches!(entry.get_password(), Err(Error::NoEntry)));
};
handles.push(std::thread::spawn(test))
}
for handle in handles {
handle.join().unwrap()
}
}
#[test]
fn test_multiple_create_delete_single_thread() {
let name = generate_random_string();
let entry = entry_new(&name, &name);
let repeats = 10;
for _i in 0..repeats {
entry.set_password(&name).unwrap();
let stored_password = entry.get_password().unwrap();
assert_eq!(stored_password, name);
entry.delete_credential().unwrap();
assert!(matches!(entry.get_password(), Err(Error::NoEntry)));
}
}
#[test]
fn test_simultaneous_multiple_create_delete_single_thread() {
let mut handles = vec![];
let base = generate_random_string();
for t in 0..10 {
let name = format!("{base}-{t}");
let test = move || {
let entry = entry_new(&name, &name);
let repeats = 10;
for _i in 0..repeats {
entry.set_password(&name).unwrap();
assert_eq!(entry.get_password().unwrap(), name);
entry.delete_credential().unwrap();
assert!(matches!(entry.get_password(), Err(Error::NoEntry)));
}
};
handles.push(std::thread::spawn(test))
}
for handle in handles {
handle.join().unwrap()
}
}
#[test]
fn test_credential_persistence() {
let name = generate_random_string();
let persist_local = HashMap::from([("persistence", "local")]);
let persist_session = HashMap::from([("persistence", "session")]);
let default = entry_new(&name, "enterprise");
default.set_password("enterprise").unwrap();
assert_eq!(
default.get_attributes().unwrap()["persistence"],
"Enterprise"
);
let session = entry_new_with_modifiers(&name, "session", &persist_session);
session.set_password("session").unwrap();
assert_eq!(session.get_attributes().unwrap()["persistence"], "Session");
let local = entry_new_with_modifiers(&name, "local", &persist_local);
local.set_password("local").unwrap();
assert_eq!(local.get_attributes().unwrap()["persistence"], "Local");
let mock_session = entry_new(&name, "session");
assert_eq!(mock_session.get_password().unwrap(), "session");
assert_eq!(
mock_session.get_attributes().unwrap()["persistence"],
"Session"
);
mock_session.set_password("enterprise").unwrap();
assert_eq!(
mock_session.get_attributes().unwrap()["persistence"],
"Enterprise"
);
assert_eq!(session.get_password().unwrap(), "enterprise");
assert_eq!(
session.get_attributes().unwrap()["persistence"],
"Enterprise"
);
session.set_password("back to session").unwrap();
assert_eq!(session.get_attributes().unwrap()["persistence"], "Session");
assert_eq!(mock_session.get_password().unwrap(), "back to session");
assert_eq!(
mock_session.get_attributes().unwrap()["persistence"],
"Session"
);
mock_session.delete_credential().unwrap();
session.delete_credential().unwrap_err();
local.delete_credential().unwrap();
default.delete_credential().unwrap();
}
#[cfg(feature = "search")]
#[test]
fn test_search() {
use keyring_core::api::CredentialApi;
let name = generate_random_string();
let entry = entry_new("search entry", &name);
entry.set_password("test search entry").unwrap();
let entries = Entry::search(&HashMap::from([("pattern", "test-")])).unwrap();
entry.delete_credential().unwrap();
assert!(!entries.is_empty());
println!("Found {} test entries:", entries.len());
let mut found = false;
for e in entries {
let cred: &Cred = e.as_any().downcast_ref().unwrap();
match cred.get_specifiers() {
None => panic!("All test entries should have specifiers"),
Some(specs) => {
if specs.0.ends_with("search entry") && specs.1 == name {
found = true;
println!("\t{specs:?} (test target)");
} else {
println!("\t{specs:?}");
}
}
}
}
assert!(found, "We didn't find the test search entry");
}
#[test]
fn test_store_persistence() {
let store: Arc<CredentialStore> = Store::new().unwrap();
assert!(matches!(
store.persistence(),
CredentialPersistence::UntilDelete
));
}