use std::collections::HashMap;
use std::sync::{Arc, Once};
use log::debug;
use keyring_core::{CredentialStore, Entry, Error, api::CredentialPersistence, get_default_store};
use super::keychain::{Cred, Store};
static SET_STORE: Once = Once::new();
fn usually_goes_in_main() {
let _ = env_logger::builder().is_test(true).try_init();
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(service, user).unwrap_or_else(|err| {
panic!("Couldn't create entry (service: {service}, user: {user}): {err:?}")
})
}
fn generate_random_string() -> String {
use fastrand;
use std::iter::repeat_with;
repeat_with(fastrand::alphanumeric).take(12).collect()
}
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_invalid_parameter() {
SET_STORE.call_once(usually_goes_in_main);
let entry = Entry::new("", "user");
assert!(matches!(entry, Err(Error::Invalid(_, _))));
let entry = Entry::new("service", "");
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 = "";
test_round_trip("empty password", &entry_new(&name, &name), in_pass);
}
#[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_duplicate_entries() {
let name = generate_random_string();
let entry1 = entry_new(&name, &name);
let entry2 = entry_new(&name, &name);
entry1.set_password("password for entry1").unwrap();
let password = entry2.get_password().unwrap();
assert_eq!(password, "password for entry1");
entry2.set_password("password for entry2").unwrap();
let password = entry1.get_password().unwrap();
assert_eq!(password, "password for entry2");
entry1.delete_credential().unwrap();
entry2.delete_credential().expect_err("Can delete entry2");
}
#[test]
fn test_get_credential_and_specifiers() {
let name = generate_random_string();
let entry1 = entry_new(&name, &name);
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, name);
assert_eq!(user, name);
wrapper.delete_credential().unwrap();
entry1.delete_credential().unwrap_err();
wrapper.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![];
for i in 0..10 {
let name = format!("{}-{}", generate_random_string(), 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]
fn test_simultaneous_create_set_then_move() {
let mut handles = vec![];
for i in 0..10 {
let name = format!("{}-{}", generate_random_string(), 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![];
for i in 0..10 {
let name = format!("thread_entry{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![];
for t in 0..10 {
let name = generate_random_string();
let test = move || {
let name = format!("{name}-{t}");
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)));
}
};
handles.push(std::thread::spawn(test))
}
for handle in handles {
handle.join().unwrap()
}
}
#[test]
#[ignore] fn test_search() {
SET_STORE.call_once(usually_goes_in_main);
let all = Entry::search(&HashMap::new()).unwrap();
let all_count = all.len();
debug!("Search found {all_count} generic credentials before creating any.");
let none = Entry::search(&HashMap::from([("service", ""), ("user", "")])).unwrap();
assert!(none.is_empty());
let name = generate_random_string();
let bar = format!("{name}-bar");
let bam = format!("{name}-bam");
let bam_upper = format!("{name}-BAM");
let e1 = entry_new(&name, &bar);
e1.set_password("e1").unwrap();
let e2 = entry_new(&name, &bam);
e2.set_password("e2").unwrap();
let zero = Entry::search(&HashMap::from([("user", bam_upper.as_str())])).unwrap();
assert_eq!(zero.len(), 0);
let one = Entry::search(&HashMap::from([("user", bam.as_str())])).unwrap();
assert_eq!(one.len(), 1);
let one = Entry::search(&HashMap::from([
("service", name.as_str()),
("user", bar.as_ref()),
]))
.unwrap();
assert_eq!(one.len(), 1);
let two = Entry::search(&HashMap::from([("service", name.as_str())])).unwrap();
assert_eq!(two.len(), 2);
e1.delete_credential().unwrap();
e2.delete_credential().unwrap();
}
#[test]
fn test_persistence() {
let store: Arc<CredentialStore> = Store::new().unwrap();
assert!(matches!(
store.persistence(),
CredentialPersistence::UntilDelete
));
}