uv/crates/uv-keyring/tests/threading.rs
John Mumm 880eb286e8
Add new uv-keyring crate that vendors the keyring-rs crate (#14725)
This PR is a first step toward support for storing credentials in the
system keyring. The `keyring-rs` crate is the best option for system
keyring integration, but the latest version (v4) requires either that
Linux users have `libdbus` installed or that it is built with `libdbus`
vendored in. This is because v4 depends on
[dbus-secret-service](https://github.com/open-source-cooperative/dbus-secret-service),
which was created as an alternative to
[secret-service](https://github.com/open-source-cooperative/secret-service-rs)
so that users are not required to use an async runtime. Since uv does
use an async runtime, this is not a good tradeoff for uv.

This PR:
* Vendors `keyring-rs` crate into a new `uv-keyring` workspace crate
* Moves to the async `secret-service` crate that does not require
clients on Linux to have `libdbus` on their machines. This includes
updating `CredentialsAPI` trait (and implementations) to use async
methods.
* Adds `uv-keyring` tests to `cargo test` jobs. For `cargo test |
ubuntu`, this meant setting up secret service and priming gnome-keyring
as an earlier step.
* Removes iOS code paths
* Patches in @oconnor663 's changes from his [`keyring-rs`
PR](https://github.com/open-source-cooperative/keyring-rs/pull/261)
* Applies many clippy-driven updates
2025-08-15 15:57:56 +02:00

280 lines
8.3 KiB
Rust

#![cfg(feature = "keyring-tests")]
use common::{generate_random_string, init_logger};
use uv_keyring::{Entry, Error};
mod common;
#[tokio::test]
async fn test_create_then_move() {
init_logger();
let name = generate_random_string();
let entry = Entry::new(&name, &name).unwrap();
let handle = tokio::spawn(async move {
let password = "test ascii password";
entry
.set_password(password)
.await
.expect("Can't set initial ascii password");
let stored_password = entry
.get_password()
.await
.expect("Can't get ascii password");
assert_eq!(
stored_password, password,
"Retrieved and set initial ascii passwords don't match"
);
let password = "このきれいな花は桜です";
entry
.set_password(password)
.await
.expect("Can't set non-ascii password");
let stored_password = entry
.get_password()
.await
.expect("Can't get non-ascii password");
assert_eq!(
stored_password, password,
"Retrieved and set non-ascii passwords don't match"
);
entry
.delete_credential()
.await
.expect("Can't delete non-ascii password");
assert!(
matches!(entry.get_password().await, Err(Error::NoEntry)),
"Able to read a deleted non-ascii password"
);
});
handle.await.expect("Task failed");
}
#[tokio::test]
async fn test_simultaneous_create_then_move() {
init_logger();
let mut handles = vec![];
for i in 0..10 {
let name = format!("{}-{}", generate_random_string(), i);
let entry = Entry::new(&name, &name).expect("Can't create entry");
let handle = tokio::spawn(async move {
entry
.set_password(&name)
.await
.expect("Can't set ascii password");
let stored_password = entry
.get_password()
.await
.expect("Can't get ascii password");
assert_eq!(
stored_password, name,
"Retrieved and set ascii passwords don't match"
);
entry
.delete_credential()
.await
.expect("Can't delete ascii password");
assert!(
matches!(entry.get_password().await, Err(Error::NoEntry)),
"Able to read a deleted ascii password"
);
});
handles.push(handle);
}
for handle in handles {
handle.await.expect("Task failed");
}
}
#[tokio::test]
#[cfg(not(target_os = "windows"))]
async fn test_create_set_then_move() {
init_logger();
let name = generate_random_string();
let entry = Entry::new(&name, &name).expect("Can't create entry");
let password = "test ascii password";
entry
.set_password(password)
.await
.expect("Can't set ascii password");
let handle = tokio::spawn(async move {
let stored_password = entry
.get_password()
.await
.expect("Can't get ascii password");
assert_eq!(
stored_password, password,
"Retrieved and set ascii passwords don't match"
);
entry
.delete_credential()
.await
.expect("Can't delete ascii password");
assert!(
matches!(entry.get_password().await, Err(Error::NoEntry)),
"Able to read a deleted ascii password"
);
});
handle.await.expect("Task failed");
}
#[tokio::test]
#[cfg(not(target_os = "windows"))]
async fn test_simultaneous_create_set_then_move() {
init_logger();
let mut handles = vec![];
for i in 0..10 {
let name = format!("{}-{}", generate_random_string(), i);
let entry = Entry::new(&name, &name).expect("Can't create entry");
entry
.set_password(&name)
.await
.expect("Can't set ascii password");
let handle = tokio::spawn(async move {
let stored_password = entry
.get_password()
.await
.expect("Can't get ascii password");
assert_eq!(
stored_password, name,
"Retrieved and set ascii passwords don't match"
);
entry
.delete_credential()
.await
.expect("Can't delete ascii password");
assert!(
matches!(entry.get_password().await, Err(Error::NoEntry)),
"Able to read a deleted ascii password"
);
});
handles.push(handle);
}
for handle in handles {
handle.await.expect("Task failed");
}
}
#[tokio::test]
async fn test_simultaneous_independent_create_set() {
init_logger();
let mut handles = vec![];
for i in 0..10 {
let name = format!("thread_entry{i}");
let handle = tokio::spawn(async move {
let entry = Entry::new(&name, &name).expect("Can't create entry");
entry
.set_password(&name)
.await
.expect("Can't set ascii password");
let stored_password = entry
.get_password()
.await
.expect("Can't get ascii password");
assert_eq!(
stored_password, name,
"Retrieved and set ascii passwords don't match"
);
entry
.delete_credential()
.await
.expect("Can't delete ascii password");
assert!(
matches!(entry.get_password().await, Err(Error::NoEntry)),
"Able to read a deleted ascii password"
);
});
handles.push(handle);
}
for handle in handles {
handle.await.expect("Task failed");
}
}
#[tokio::test]
#[cfg(any(target_os = "macos", target_os = "windows"))]
async fn test_multiple_create_delete_single_thread() {
init_logger();
let name = generate_random_string();
let entry = Entry::new(&name, &name).expect("Can't create entry");
let repeats = 10;
for _i in 0..repeats {
entry
.set_password(&name)
.await
.expect("Can't set ascii password");
let stored_password = entry
.get_password()
.await
.expect("Can't get ascii password");
assert_eq!(
stored_password, name,
"Retrieved and set ascii passwords don't match"
);
entry
.delete_credential()
.await
.expect("Can't delete ascii password");
assert!(
matches!(entry.get_password().await, Err(Error::NoEntry)),
"Able to read a deleted ascii password"
);
}
}
#[tokio::test]
#[cfg(any(target_os = "macos", target_os = "windows"))]
async fn test_simultaneous_multiple_create_delete_single_thread() {
init_logger();
let mut handles = vec![];
for t in 0..10 {
let name = generate_random_string();
let handle = tokio::spawn(async move {
let name = format!("{name}-{t}");
let entry = Entry::new(&name, &name).expect("Can't create entry");
let repeats = 10;
for _i in 0..repeats {
entry
.set_password(&name)
.await
.expect("Can't set ascii password");
let stored_password = entry
.get_password()
.await
.expect("Can't get ascii password");
assert_eq!(
stored_password, name,
"Retrieved and set ascii passwords don't match"
);
entry
.delete_credential()
.await
.expect("Can't delete ascii password");
assert!(
matches!(entry.get_password().await, Err(Error::NoEntry)),
"Able to read a deleted ascii password"
);
}
});
handles.push(handle);
}
for handle in handles {
handle.await.expect("Task failed");
}
}