mirror of https://github.com/astral-sh/uv
Update tests and examples
This commit is contained in:
parent
243ae0bcee
commit
68b6a6f14b
|
|
@ -0,0 +1,335 @@
|
||||||
|
extern crate uv_keyring;
|
||||||
|
|
||||||
|
use clap::{Args, Parser};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use uv_keyring::{Entry, Error, Result};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let mut args: Cli = Cli::parse();
|
||||||
|
if args.user.eq_ignore_ascii_case("<logged-in username>") {
|
||||||
|
args.user = whoami::username()
|
||||||
|
}
|
||||||
|
let entry = match args.entry_for() {
|
||||||
|
Ok(entry) => entry,
|
||||||
|
Err(err) => {
|
||||||
|
if args.verbose {
|
||||||
|
let description = args.description();
|
||||||
|
eprintln!("Couldn't create entry for '{description}': {err}")
|
||||||
|
}
|
||||||
|
std::process::exit(1)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match &args.command {
|
||||||
|
Command::Set { .. } => {
|
||||||
|
let value = args.get_password_and_attributes();
|
||||||
|
match &value {
|
||||||
|
Value::Secret(secret) => match entry.set_secret(secret) {
|
||||||
|
Ok(()) => args.success_message_for(&value),
|
||||||
|
Err(err) => args.error_message_for(err),
|
||||||
|
},
|
||||||
|
Value::Password(password) => match entry.set_password(password) {
|
||||||
|
Ok(()) => args.success_message_for(&value),
|
||||||
|
Err(err) => args.error_message_for(err),
|
||||||
|
},
|
||||||
|
Value::Attributes(attributes) => {
|
||||||
|
let attrs: HashMap<&str, &str> = attributes
|
||||||
|
.iter()
|
||||||
|
.map(|(k, v)| (k.as_str(), v.as_str()))
|
||||||
|
.collect();
|
||||||
|
match entry.update_attributes(&attrs) {
|
||||||
|
Ok(()) => args.success_message_for(&value),
|
||||||
|
Err(err) => args.error_message_for(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => panic!("Can't set without a value"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Command::Password => match entry.get_password() {
|
||||||
|
Ok(password) => {
|
||||||
|
println!("{password}");
|
||||||
|
args.success_message_for(&Value::Password(password));
|
||||||
|
}
|
||||||
|
Err(err) => args.error_message_for(err),
|
||||||
|
},
|
||||||
|
Command::Secret => match entry.get_secret() {
|
||||||
|
Ok(secret) => {
|
||||||
|
println!("{}", secret_string(&secret));
|
||||||
|
args.success_message_for(&Value::Secret(secret));
|
||||||
|
}
|
||||||
|
Err(err) => args.error_message_for(err),
|
||||||
|
},
|
||||||
|
Command::Attributes => match entry.get_attributes() {
|
||||||
|
Ok(attributes) => {
|
||||||
|
println!("{}", attributes_string(&attributes));
|
||||||
|
args.success_message_for(&Value::Attributes(attributes));
|
||||||
|
}
|
||||||
|
Err(err) => args.error_message_for(err),
|
||||||
|
},
|
||||||
|
Command::Delete => match entry.delete_credential() {
|
||||||
|
Ok(()) => args.success_message_for(&Value::None),
|
||||||
|
Err(err) => args.error_message_for(err),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
#[clap(author = "github.com/hwchen/keyring-rs")]
|
||||||
|
/// Keyring CLI: A command-line interface to platform secure storage
|
||||||
|
pub struct Cli {
|
||||||
|
#[clap(short, long, action, verbatim_doc_comment)]
|
||||||
|
/// Write debugging info to stderr, including retrieved passwords and secrets.
|
||||||
|
/// If an operation fails, detailed error information is provided.
|
||||||
|
pub verbose: bool,
|
||||||
|
|
||||||
|
#[clap(short, long, value_parser)]
|
||||||
|
/// The (optional) target for the entry.
|
||||||
|
pub target: Option<String>,
|
||||||
|
|
||||||
|
#[clap(short, long, value_parser, default_value = "keyring-cli")]
|
||||||
|
/// The service for the entry.
|
||||||
|
pub service: String,
|
||||||
|
|
||||||
|
#[clap(short, long, value_parser, default_value = "<logged-in username>")]
|
||||||
|
/// The user for the entry.
|
||||||
|
pub user: String,
|
||||||
|
|
||||||
|
#[clap(subcommand)]
|
||||||
|
pub command: Command,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
pub enum Command {
|
||||||
|
/// Set the password or update the attributes in the secure store
|
||||||
|
Set {
|
||||||
|
#[command(flatten)]
|
||||||
|
what: What,
|
||||||
|
|
||||||
|
#[clap(value_parser)]
|
||||||
|
/// The input to parse. If not specified, it will be
|
||||||
|
/// read interactively from the terminal. Password/secret
|
||||||
|
/// input will not be echoed.
|
||||||
|
input: Option<String>,
|
||||||
|
},
|
||||||
|
/// Retrieve the (string) password from the secure store
|
||||||
|
/// and write it to the standard output.
|
||||||
|
Password,
|
||||||
|
/// Retrieve the (binary) secret from the secure store
|
||||||
|
/// and write it in base64 encoding to the standard output.
|
||||||
|
Secret,
|
||||||
|
/// Retrieve attributes available in the secure store.
|
||||||
|
Attributes,
|
||||||
|
/// Delete the credential from the secure store.
|
||||||
|
Delete,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Args)]
|
||||||
|
#[group(multiple = false, required = true)]
|
||||||
|
pub struct What {
|
||||||
|
#[clap(short, long, action, help = "The input is a password")]
|
||||||
|
password: bool,
|
||||||
|
|
||||||
|
#[clap(short, long, action, help = "The input is a base64-encoded secret")]
|
||||||
|
secret: bool,
|
||||||
|
|
||||||
|
#[clap(
|
||||||
|
short,
|
||||||
|
long,
|
||||||
|
action,
|
||||||
|
help = "The input is comma-separated, key=val attribute pairs"
|
||||||
|
)]
|
||||||
|
attributes: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Value {
|
||||||
|
Secret(Vec<u8>),
|
||||||
|
Password(String),
|
||||||
|
Attributes(HashMap<String, String>),
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cli {
|
||||||
|
fn description(&self) -> String {
|
||||||
|
if let Some(target) = &self.target {
|
||||||
|
format!("{}@{}:{target}", &self.user, &self.service)
|
||||||
|
} else {
|
||||||
|
format!("{}@{}", &self.user, &self.service)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn entry_for(&self) -> Result<Entry> {
|
||||||
|
if let Some(target) = &self.target {
|
||||||
|
Entry::new_with_target(target, &self.service, &self.user)
|
||||||
|
} else {
|
||||||
|
Entry::new(&self.service, &self.user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn error_message_for(&self, err: Error) {
|
||||||
|
if self.verbose {
|
||||||
|
let description = self.description();
|
||||||
|
match err {
|
||||||
|
Error::NoEntry => {
|
||||||
|
eprintln!("No credential found for '{description}'");
|
||||||
|
}
|
||||||
|
Error::Ambiguous(creds) => {
|
||||||
|
eprintln!("More than one credential found for '{description}': {creds:?}");
|
||||||
|
}
|
||||||
|
err => match self.command {
|
||||||
|
Command::Set { .. } => {
|
||||||
|
eprintln!("Couldn't set credential data for '{description}': {err}");
|
||||||
|
}
|
||||||
|
Command::Password => {
|
||||||
|
eprintln!("Couldn't get password for '{description}': {err}");
|
||||||
|
}
|
||||||
|
Command::Secret => {
|
||||||
|
eprintln!("Couldn't get secret for '{description}': {err}");
|
||||||
|
}
|
||||||
|
Command::Attributes => {
|
||||||
|
eprintln!("Couldn't get attributes for '{description}': {err}");
|
||||||
|
}
|
||||||
|
Command::Delete => {
|
||||||
|
eprintln!("Couldn't delete credential for '{description}': {err}");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::process::exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn success_message_for(&self, value: &Value) {
|
||||||
|
if !self.verbose {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let description = self.description();
|
||||||
|
match self.command {
|
||||||
|
Command::Set { .. } => match value {
|
||||||
|
Value::Secret(secret) => {
|
||||||
|
let secret = secret_string(secret);
|
||||||
|
eprintln!("Set secret for '{description}' to decode of '{secret}'");
|
||||||
|
}
|
||||||
|
Value::Password(password) => {
|
||||||
|
eprintln!("Set password for '{description}' to '{password}'");
|
||||||
|
}
|
||||||
|
Value::Attributes(attributes) => {
|
||||||
|
eprintln!("The following attributes for '{description}' were sent for update:");
|
||||||
|
eprint_attributes(attributes);
|
||||||
|
}
|
||||||
|
_ => panic!("Can't set without a value"),
|
||||||
|
},
|
||||||
|
Command::Password => {
|
||||||
|
match value {
|
||||||
|
Value::Password(password) => {
|
||||||
|
eprintln!("Password for '{description}' is '{password}'");
|
||||||
|
}
|
||||||
|
_ => panic!("Wrong value type for command"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Command::Secret => match value {
|
||||||
|
Value::Secret(secret) => {
|
||||||
|
let encoded = secret_string(secret);
|
||||||
|
eprintln!("Secret for '{description}' encodes as {encoded}");
|
||||||
|
}
|
||||||
|
_ => panic!("Wrong value type for command"),
|
||||||
|
},
|
||||||
|
Command::Attributes => match value {
|
||||||
|
Value::Attributes(attributes) => {
|
||||||
|
if attributes.is_empty() {
|
||||||
|
eprintln!("No attributes found for '{description}'");
|
||||||
|
} else {
|
||||||
|
eprintln!("Attributes for '{description}' are:");
|
||||||
|
eprint_attributes(attributes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => panic!("Wrong value type for command"),
|
||||||
|
},
|
||||||
|
Command::Delete => {
|
||||||
|
eprintln!("Successfully deleted credential for '{description}'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_password_and_attributes(&self) -> Value {
|
||||||
|
if let Command::Set { what, input } = &self.command {
|
||||||
|
if what.password {
|
||||||
|
Value::Password(read_password(input))
|
||||||
|
} else if what.secret {
|
||||||
|
Value::Secret(decode_secret(input))
|
||||||
|
} else {
|
||||||
|
Value::Attributes(parse_attributes(input))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic!("Can't happen: asking for password and attributes on non-set command")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn secret_string(secret: &[u8]) -> String {
|
||||||
|
use base64::prelude::*;
|
||||||
|
|
||||||
|
BASE64_STANDARD.encode(secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn eprint_attributes(attributes: &HashMap<String, String>) {
|
||||||
|
for (key, value) in attributes {
|
||||||
|
println!(" {key}: {value}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_secret(input: &Option<String>) -> Vec<u8> {
|
||||||
|
use base64::prelude::*;
|
||||||
|
|
||||||
|
let encoded = if let Some(input) = input {
|
||||||
|
input.clone()
|
||||||
|
} else {
|
||||||
|
rpassword::prompt_password("Base64 encoding: ").unwrap_or_else(|_| String::new())
|
||||||
|
};
|
||||||
|
if encoded.is_empty() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
match BASE64_STANDARD.decode(encoded) {
|
||||||
|
Ok(secret) => secret,
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("Sorry, the provided secret data is not base64-encoded: {err}");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_password(input: &Option<String>) -> String {
|
||||||
|
if let Some(input) = input {
|
||||||
|
input.clone()
|
||||||
|
} else {
|
||||||
|
rpassword::prompt_password("Password: ").unwrap_or_else(|_| String::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn attributes_string(attributes: &HashMap<String, String>) -> String {
|
||||||
|
let strings = attributes
|
||||||
|
.iter()
|
||||||
|
.map(|(k, v)| format!("{}={}", k, v))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
strings.join(",")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_attributes(input: &Option<String>) -> HashMap<String, String> {
|
||||||
|
let input = if let Some(input) = input {
|
||||||
|
input.clone()
|
||||||
|
} else {
|
||||||
|
rprompt::prompt_reply("Attributes: ").unwrap_or_else(|_| String::new())
|
||||||
|
};
|
||||||
|
if input.is_empty() {
|
||||||
|
eprintln!("You must specify at least one key=value attribute pair to set")
|
||||||
|
}
|
||||||
|
let mut attributes = HashMap::new();
|
||||||
|
let parts = input.split(',');
|
||||||
|
for s in parts.into_iter() {
|
||||||
|
let parts: Vec<&str> = s.split("=").collect();
|
||||||
|
if parts.len() != 2 || parts[0].is_empty() {
|
||||||
|
eprintln!("Sorry, this part of the attributes string is not a key=val pair: {s}");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
attributes.insert(parts[0].to_string(), parts[1].to_string());
|
||||||
|
}
|
||||||
|
attributes
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,116 @@
|
||||||
|
use uv_keyring::{Entry, Error};
|
||||||
|
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
extern "C" fn test() {
|
||||||
|
test_invalid_parameter();
|
||||||
|
test_empty_keyring();
|
||||||
|
test_empty_password_input();
|
||||||
|
test_round_trip_ascii_password();
|
||||||
|
test_round_trip_non_ascii_password();
|
||||||
|
test_update_password();
|
||||||
|
#[cfg(target_os = "ios")]
|
||||||
|
test_get_credential();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_invalid_parameter() {
|
||||||
|
let entry = Entry::new("", "user");
|
||||||
|
assert!(
|
||||||
|
matches!(entry, Err(Error::Invalid(_, _))),
|
||||||
|
"Created entry with empty service"
|
||||||
|
);
|
||||||
|
let entry = Entry::new("service", "");
|
||||||
|
assert!(
|
||||||
|
matches!(entry, Err(Error::Invalid(_, _))),
|
||||||
|
"Created entry with empty user"
|
||||||
|
);
|
||||||
|
let entry = Entry::new_with_target("test", "service", "user");
|
||||||
|
assert!(
|
||||||
|
matches!(entry, Err(Error::Invalid(_, _))),
|
||||||
|
"Created entry with non-default target"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_empty_keyring() {
|
||||||
|
let name = "test_empty_keyring".to_string();
|
||||||
|
let entry = Entry::new(&name, &name).expect("Failed to create entry");
|
||||||
|
assert!(matches!(entry.get_password(), Err(Error::NoEntry)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_empty_password_input() {
|
||||||
|
let name = "test_empty_password_input".to_string();
|
||||||
|
let entry = Entry::new(&name, &name).expect("Failed to create entry");
|
||||||
|
let in_pass = "";
|
||||||
|
entry
|
||||||
|
.set_password(in_pass)
|
||||||
|
.expect("Couldn't set empty password");
|
||||||
|
let out_pass = entry.get_password().expect("Couldn't get empty password");
|
||||||
|
assert_eq!(in_pass, out_pass);
|
||||||
|
entry
|
||||||
|
.delete_credential()
|
||||||
|
.expect("Couldn't delete credential with empty password");
|
||||||
|
assert!(
|
||||||
|
matches!(entry.get_password(), Err(Error::NoEntry)),
|
||||||
|
"Able to read a deleted password"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_round_trip_ascii_password() {
|
||||||
|
let name = "test_round_trip_ascii_password".to_string();
|
||||||
|
let entry = Entry::new(&name, &name).expect("Failed to create entry");
|
||||||
|
let password = "test ascii 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)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_round_trip_non_ascii_password() {
|
||||||
|
let name = "test_round_trip_non_ascii_password".to_string();
|
||||||
|
let entry = Entry::new(&name, &name).expect("Failed to create entry");
|
||||||
|
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)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_update_password() {
|
||||||
|
let name = "test_update_password".to_string();
|
||||||
|
let entry = Entry::new(&name, &name).expect("Failed to create entry");
|
||||||
|
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)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "ios")]
|
||||||
|
fn test_get_credential() {
|
||||||
|
use keyring::ios::IosCredential;
|
||||||
|
let name = "test_get_credential".to_string();
|
||||||
|
let entry = Entry::new(&name, &name).expect("Can't create entry for get_credential");
|
||||||
|
let credential: &IosCredential = entry
|
||||||
|
.get_credential()
|
||||||
|
.downcast_ref()
|
||||||
|
.expect("Not an iOS credential");
|
||||||
|
assert!(
|
||||||
|
credential.get_credential().is_err(),
|
||||||
|
"Platform credential shouldn't exist yet!"
|
||||||
|
);
|
||||||
|
entry
|
||||||
|
.set_password("test get password for get_credential")
|
||||||
|
.expect("Can't get password for get_credential");
|
||||||
|
assert!(credential.get_credential().is_ok());
|
||||||
|
entry.delete_credential().unwrap();
|
||||||
|
assert!(
|
||||||
|
matches!(entry.get_password(), Err(Error::NoEntry)),
|
||||||
|
"Platform credential exists after delete password"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -122,7 +122,7 @@ store allows for pre-setting errors as well as password values to
|
||||||
be returned from [Entry] method calls. If you want to use the mock
|
be returned from [Entry] method calls. If you want to use the mock
|
||||||
credential store as your default in tests, make this call:
|
credential store as your default in tests, make this call:
|
||||||
```
|
```
|
||||||
keyring::set_default_credential_builder(keyring::mock::default_credential_builder())
|
uv_keyring::set_default_credential_builder(uv_keyring::mock::default_credential_builder())
|
||||||
```
|
```
|
||||||
|
|
||||||
## Interoperability with Third Parties
|
## Interoperability with Third Parties
|
||||||
|
|
@ -442,7 +442,7 @@ mod tests {
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
/// Create a platform-specific credential given the constructor, service, and user
|
/// Create a platform-specific credential given the constructor, service, and user
|
||||||
pub fn entry_from_constructor<F, T>(f: F, service: &str, user: &str) -> Entry
|
pub(crate) fn entry_from_constructor<F, T>(f: F, service: &str, user: &str) -> Entry
|
||||||
where
|
where
|
||||||
F: FnOnce(Option<&str>, &str, &str) -> Result<T>,
|
F: FnOnce(Option<&str>, &str, &str) -> Result<T>,
|
||||||
T: 'static + CredentialApi + Send + Sync,
|
T: 'static + CredentialApi + Send + Sync,
|
||||||
|
|
@ -456,7 +456,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a platform-specific credential given the constructor, service, user, and attributes
|
/// Create a platform-specific credential given the constructor, service, user, and attributes
|
||||||
pub fn entry_from_constructor_and_attributes<F, T>(
|
pub(crate) fn entry_from_constructor_and_attributes<F, T>(
|
||||||
f: F,
|
f: F,
|
||||||
service: &str,
|
service: &str,
|
||||||
user: &str,
|
user: &str,
|
||||||
|
|
@ -488,7 +488,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A basic round-trip unit test given an entry and a password.
|
/// A basic round-trip unit test given an entry and a password.
|
||||||
pub fn test_round_trip(case: &str, entry: &Entry, in_pass: &str) {
|
pub(crate) fn test_round_trip(case: &str, entry: &Entry, in_pass: &str) {
|
||||||
test_round_trip_no_delete(case, entry, in_pass);
|
test_round_trip_no_delete(case, entry, in_pass);
|
||||||
entry
|
entry
|
||||||
.delete_credential()
|
.delete_credential()
|
||||||
|
|
@ -501,7 +501,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A basic round-trip unit test given an entry and a password.
|
/// A basic round-trip unit test given an entry and a password.
|
||||||
pub fn test_round_trip_secret(case: &str, entry: &Entry, in_secret: &[u8]) {
|
pub(crate) fn test_round_trip_secret(case: &str, entry: &Entry, in_secret: &[u8]) {
|
||||||
entry
|
entry
|
||||||
.set_secret(in_secret)
|
.set_secret(in_secret)
|
||||||
.unwrap_or_else(|err| panic!("Can't set secret for {case}: {err:?}"));
|
.unwrap_or_else(|err| panic!("Can't set secret for {case}: {err:?}"));
|
||||||
|
|
@ -528,13 +528,13 @@ mod tests {
|
||||||
/// to have tests use a random string for key names to avoid
|
/// to have tests use a random string for key names to avoid
|
||||||
/// the conflicts, and then do any needed cleanup once everything
|
/// the conflicts, and then do any needed cleanup once everything
|
||||||
/// is working correctly. So we export this function for tests to use.
|
/// is working correctly. So we export this function for tests to use.
|
||||||
pub fn generate_random_string_of_len(len: usize) -> String {
|
pub(crate) fn generate_random_string_of_len(len: usize) -> String {
|
||||||
use fastrand;
|
use fastrand;
|
||||||
use std::iter::repeat_with;
|
use std::iter::repeat_with;
|
||||||
repeat_with(fastrand::alphanumeric).take(len).collect()
|
repeat_with(fastrand::alphanumeric).take(len).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_random_string() -> String {
|
pub(crate) fn generate_random_string() -> String {
|
||||||
generate_random_string_of_len(30)
|
generate_random_string_of_len(30)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -544,18 +544,7 @@ mod tests {
|
||||||
repeat_with(|| fastrand::u8(..)).take(len).collect()
|
repeat_with(|| fastrand::u8(..)).take(len).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn test_empty_service_and_user<F>(f: F)
|
pub(crate) async fn test_missing_entry<F>(f: F)
|
||||||
where
|
|
||||||
F: Fn(&str, &str) -> Entry,
|
|
||||||
{
|
|
||||||
let name = generate_random_string();
|
|
||||||
let in_pass = "doesn't matter";
|
|
||||||
test_round_trip("empty user", &f(&name, ""), in_pass);
|
|
||||||
test_round_trip("empty service", &f("", &name), in_pass);
|
|
||||||
test_round_trip("empty service & user", &f("", ""), in_pass);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn test_missing_entry<F>(f: F)
|
|
||||||
where
|
where
|
||||||
F: FnOnce(&str, &str) -> Entry,
|
F: FnOnce(&str, &str) -> Entry,
|
||||||
{
|
{
|
||||||
|
|
@ -567,7 +556,7 @@ mod tests {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn test_empty_password<F>(f: F)
|
pub(crate) fn test_empty_password<F>(f: F)
|
||||||
where
|
where
|
||||||
F: FnOnce(&str, &str) -> Entry,
|
F: FnOnce(&str, &str) -> Entry,
|
||||||
{
|
{
|
||||||
|
|
@ -576,7 +565,7 @@ mod tests {
|
||||||
test_round_trip("empty password", &entry, "");
|
test_round_trip("empty password", &entry, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn test_round_trip_ascii_password<F>(f: F)
|
pub(crate) fn test_round_trip_ascii_password<F>(f: F)
|
||||||
where
|
where
|
||||||
F: FnOnce(&str, &str) -> Entry,
|
F: FnOnce(&str, &str) -> Entry,
|
||||||
{
|
{
|
||||||
|
|
@ -585,7 +574,7 @@ mod tests {
|
||||||
test_round_trip("ascii password", &entry, "test ascii password");
|
test_round_trip("ascii password", &entry, "test ascii password");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn test_round_trip_non_ascii_password<F>(f: F)
|
pub(crate) fn test_round_trip_non_ascii_password<F>(f: F)
|
||||||
where
|
where
|
||||||
F: FnOnce(&str, &str) -> Entry,
|
F: FnOnce(&str, &str) -> Entry,
|
||||||
{
|
{
|
||||||
|
|
@ -594,7 +583,7 @@ mod tests {
|
||||||
test_round_trip("non-ascii password", &entry, "このきれいな花は桜です");
|
test_round_trip("non-ascii password", &entry, "このきれいな花は桜です");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn test_round_trip_random_secret<F>(f: F)
|
pub(crate) fn test_round_trip_random_secret<F>(f: F)
|
||||||
where
|
where
|
||||||
F: FnOnce(&str, &str) -> Entry,
|
F: FnOnce(&str, &str) -> Entry,
|
||||||
{
|
{
|
||||||
|
|
@ -604,7 +593,7 @@ mod tests {
|
||||||
test_round_trip_secret("non-ascii password", &entry, secret.as_slice());
|
test_round_trip_secret("non-ascii password", &entry, secret.as_slice());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn test_update<F>(f: F)
|
pub(crate) fn test_update<F>(f: F)
|
||||||
where
|
where
|
||||||
F: FnOnce(&str, &str) -> Entry,
|
F: FnOnce(&str, &str) -> Entry,
|
||||||
{
|
{
|
||||||
|
|
@ -618,7 +607,7 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn test_noop_get_update_attributes<F>(f: F)
|
pub(crate) fn test_noop_get_update_attributes<F>(f: F)
|
||||||
where
|
where
|
||||||
F: FnOnce(&str, &str) -> Entry,
|
F: FnOnce(&str, &str) -> Entry,
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -614,9 +614,9 @@ mod tests {
|
||||||
crate::tests::test_empty_service_and_user(entry_new);
|
crate::tests::test_empty_service_and_user(entry_new);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn test_missing_entry() {
|
async fn test_missing_entry() {
|
||||||
crate::tests::test_missing_entry(entry_new);
|
crate::tests::test_missing_entry(entry_new).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -664,9 +664,9 @@ mod tests {
|
||||||
crate::tests::test_empty_service_and_user(entry_new);
|
crate::tests::test_empty_service_and_user(entry_new);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn test_missing_entry() {
|
async fn test_missing_entry() {
|
||||||
crate::tests::test_missing_entry(entry_new);
|
crate::tests::test_missing_entry(entry_new).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -12,22 +12,22 @@
|
||||||
/// to have tests use a random string for key names to avoid
|
/// to have tests use a random string for key names to avoid
|
||||||
/// the conflicts, and then do any needed cleanup once everything
|
/// the conflicts, and then do any needed cleanup once everything
|
||||||
/// is working correctly. So we export this function for tests to use.
|
/// is working correctly. So we export this function for tests to use.
|
||||||
pub fn generate_random_string_of_len(len: usize) -> String {
|
pub(crate) fn generate_random_string_of_len(len: usize) -> String {
|
||||||
use fastrand;
|
use fastrand;
|
||||||
use std::iter::repeat_with;
|
use std::iter::repeat_with;
|
||||||
repeat_with(fastrand::alphanumeric).take(len).collect()
|
repeat_with(fastrand::alphanumeric).take(len).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_random_string() -> String {
|
pub(crate) fn generate_random_string() -> String {
|
||||||
generate_random_string_of_len(30)
|
generate_random_string_of_len(30)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_random_bytes_of_len(len: usize) -> Vec<u8> {
|
pub(crate) fn generate_random_bytes_of_len(len: usize) -> Vec<u8> {
|
||||||
use fastrand;
|
use fastrand;
|
||||||
use std::iter::repeat_with;
|
use std::iter::repeat_with;
|
||||||
repeat_with(|| fastrand::u8(..)).take(len).collect()
|
repeat_with(|| fastrand::u8(..)).take(len).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init_logger() {
|
pub(crate) fn init_logger() {
|
||||||
let _ = env_logger::builder().is_test(true).try_init();
|
let _ = env_logger::builder().is_test(true).try_init();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue