Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement integration test suite that can simulate user input #192

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
266 changes: 237 additions & 29 deletions Cargo.lock

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ tiny-bip39 = "1.0"
tokio = { version = "1.10.1", features = ["full"] }
url = "2.3"

[dev-dependencies]
enigo = "0.2.1"
lazy_static = "1.4"
tempfile = "3"

[lib]
name = "forc_wallet"
path = "src/lib.rs"
Expand Down
78 changes: 75 additions & 3 deletions src/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,14 +123,14 @@ pub(crate) struct Transfer {
maturity: Option<u64>,
}

#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq, Eq)]
enum To {
Bech32Address(Bech32Address),
HexAddress(fuel_types::Address),
}

impl FromStr for To {
type Err = &'static str;
type Err = String;

fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
if let Ok(bech32_address) = Bech32Address::from_str(s) {
Expand All @@ -139,7 +139,10 @@ impl FromStr for To {
return Ok(Self::HexAddress(hex_address));
}

Err("Invalid address '{}': address must either be in bech32 or hex")
Err(format!(
"Invalid address '{}': address must either be in bech32 or hex",
s
))
}
}

Expand Down Expand Up @@ -621,9 +624,15 @@ pub(crate) fn read_cached_addresses(wallet_ciphertext: &[u8]) -> Result<AccountA

#[cfg(test)]
mod tests {
use std::str::FromStr;

use fuels::types::bech32::Bech32Address;

use crate::account;
use crate::utils::test_utils::{with_tmp_dir_and_wallet, TEST_PASSWORD};

use super::To;

#[test]
fn create_new_account() {
with_tmp_dir_and_wallet(|_dir, wallet_path| {
Expand Down Expand Up @@ -658,4 +667,67 @@ mod tests {
plain_address
)
}

#[test]
fn parse_to_from_bech32_str() {
let bech32_str = "fuel1j78es08cyyz5n75jugal7p759ccs323etnykzpndsvhzu6399yqqpjmmd2";
let to = To::from_str(bech32_str).unwrap();

let bech32_address = Bech32Address::from_str(bech32_str).unwrap();
let expected_to = To::Bech32Address(bech32_address);
assert_eq!(to, expected_to)
}

#[test]
fn parse_to_from_hex_str_with_0x() {
let hex_str = "0xb0b695b9eb91d9597d9a98759b359f977c3c402c027ab3720aef6664bf974ce8";
let to = To::from_str(hex_str).unwrap();

let hex_address = fuel_types::Address::from_str(hex_str).unwrap();
let expected_to = To::HexAddress(hex_address);

assert_eq!(to, expected_to)
}

#[test]
fn parse_to_from_hex_str_without_0x() {
let hex_str = "b0b695b9eb91d9597d9a98759b359f977c3c402c027ab3720aef6664bf974ce8";
let to = To::from_str(hex_str).unwrap();

let hex_address = fuel_types::Address::from_str(hex_str).unwrap();
let expected_to = To::HexAddress(hex_address);

assert_eq!(to, expected_to)
}

#[test]
fn parse_to_error_msg() {
let invalid_str = "asd";
let err = To::from_str(invalid_str).unwrap_err();
let root_cause = format!("{}", err);

let expected_err = format!(
"Invalid address '{}': address must either be in bech32 or hex",
invalid_str
);
assert_eq!(root_cause, expected_err)
}

#[test]
fn display_to_bech32() {
let bech32_str = "fuel1j78es08cyyz5n75jugal7p759ccs323etnykzpndsvhzu6399yqqpjmmd2";
let to = To::from_str(bech32_str).unwrap();
let to_str = format!("{}", to);
assert_eq!(to_str, bech32_str)
}

#[test]
fn display_to_hex() {
let hex_str = "b0b695b9eb91d9597d9a98759b359f977c3c402c027ab3720aef6664bf974ce8";
let to = To::from_str(hex_str).unwrap();
let to_str = format!("{}", to);
let expected_hex = format!("0x{}", hex_str);

assert_eq!(to_str, expected_hex)
}
}
24 changes: 16 additions & 8 deletions src/new.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ pub struct New {
#[clap(short, long)]
pub force: bool,

/// How many accounts to cache by default (Default 10)
/// How many accounts to cache by default (Default 1)
#[clap(short, long)]
pub cache_accounts: Option<usize>,
}
Expand All @@ -25,13 +25,10 @@ pub fn new_wallet_cli(wallet_path: &Path, new: New) -> anyhow::Result<()> {
ensure_no_wallet_exists(wallet_path, new.force, stdin().lock())?;
let password = request_new_password();
// Generate a random mnemonic phrase.
let mnemonic = generate_mnemonic_phrase(&mut rand::thread_rng(), 24)?;
write_wallet_from_mnemonic_and_password(wallet_path, &mnemonic, &password)?;

derive_and_cache_addresses(
&load_wallet(wallet_path)?,
&mnemonic,
0..new.cache_accounts.unwrap_or(DEFAULT_CACHE_ACCOUNTS),
let mnemonic = new_wallet(
wallet_path,
&password,
new.cache_accounts.unwrap_or(DEFAULT_CACHE_ACCOUNTS),
)?;

let mnemonic_string = format!("Wallet mnemonic phrase: {mnemonic}\n");
Expand All @@ -41,3 +38,14 @@ pub fn new_wallet_cli(wallet_path: &Path, new: New) -> anyhow::Result<()> {
)?;
Ok(())
}

pub fn new_wallet(
wallet_path: &Path,
password: &str,
cache_count: usize,
) -> anyhow::Result<String> {
let mnemonic = generate_mnemonic_phrase(&mut rand::thread_rng(), 24)?;
write_wallet_from_mnemonic_and_password(wallet_path, &mnemonic, &password)?;
derive_and_cache_addresses(&load_wallet(wallet_path)?, &mnemonic, 0..cache_count)?;
Ok(mnemonic)
}
36 changes: 33 additions & 3 deletions src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ mod tests {
}

#[test]
fn test_ensure_no_wallet_exists_no_wallet() {
fn ensure_no_wallet_exists_no_wallet() {
with_tmp_dir(|tmp_dir| {
let wallet_path = tmp_dir.join("wallet.json");
remove_wallet(&wallet_path);
Expand All @@ -276,7 +276,7 @@ mod tests {

#[test]
#[should_panic]
fn test_ensure_no_wallet_exists_throws_err() {
fn ensure_no_wallet_exists_throws_err() {
with_tmp_dir(|tmp_dir| {
let wallet_path = tmp_dir.join("wallet.json");
create_wallet(&wallet_path);
Expand All @@ -285,7 +285,7 @@ mod tests {
}

#[test]
fn test_ensure_no_wallet_exists_exists_wallet() {
fn ensure_no_wallet_exists_exists_wallet() {
// case: wallet path exist without --force and input[yes]
with_tmp_dir(|tmp_dir| {
let wallet_path = tmp_dir.join("wallet.json");
Expand All @@ -306,6 +306,36 @@ mod tests {
ensure_no_wallet_exists(&diff_wallet_path, false, &INPUT_NOP[..]).unwrap();
});
}

#[test]
fn load_wallet_fails_without_wallet() {
with_tmp_dir(|tmp_dir| {
let wallet_path = tmp_dir.join("wallet.json");
let err = load_wallet(&wallet_path).unwrap_err();
let root_cause = format!("{}", err.root_cause());
let expected_error_msg_start = format!("Failed to load a wallet from {wallet_path:?}");
let expected_error_msg_end = r"Please be sure to initialize a wallet before creating an account.
To initialize a wallet, use `forc-wallet new`";
assert!(root_cause.starts_with(&expected_error_msg_start));
assert!(root_cause.ends_with(&expected_error_msg_end));
})
}

#[test]
fn wallet_deserialization_fails() {
with_tmp_dir(|tmp_dir| {
let wallet_path = tmp_dir.join("wallet.json");
std::fs::write(&wallet_path, "this is an invalid wallet json file").unwrap();
let err = load_wallet(&wallet_path).unwrap_err();
let root_cause = format!("{}", err.root_cause());
let expected_error_msg_start =
format!("Failed to deserialize keystore from {wallet_path:?}");
let expected_error_msg_end =
format!("Please ensure that {wallet_path:?} is a valid wallet file.");
assert!(root_cause.starts_with(&expected_error_msg_start));
assert!(root_cause.ends_with(&expected_error_msg_end));
})
}
}

#[cfg(test)]
Expand Down
18 changes: 18 additions & 0 deletions tests/account.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
use anyhow::Result;
use testcfg::ForcWalletState;
pub mod testcfg;

#[test]
fn new_creates_accounts_by_default() -> Result<()> {
testcfg::setup(ForcWalletState::Initialized, &|cfg| {
let path = format!("{}", cfg.wallet_path.display());
let output = cfg.exec(&["--path", &path, "accounts", "--unverified"], &|| {});

let expected = "Account addresses (unverified, printed from cache):\n[0]";
let output_stdout = output.stdout;
dbg!(&output_stdout);
let success = output_stdout.starts_with(expected);
assert!(success)
})?;
Ok(())
}
48 changes: 48 additions & 0 deletions tests/new.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
use anyhow::Result;
use enigo::{Enigo, Settings};
use std::{thread, time::Duration};
use testcfg::ForcWalletState;

use crate::testcfg::input_utils::enter_password;

pub mod testcfg;

#[test]
#[ignore]
fn new_creates_accounts_by_default() -> Result<()> {
testcfg::setup(ForcWalletState::Initialized, &|cfg| {
let output = cfg.exec(&["accounts", "--unverified"], &|| {});

let expected = "Account addresses (unverified, printed from cache):\n[0]";
let output_stdout = output.stdout;
dbg!(&output_stdout);
let success = output_stdout.starts_with(expected);
assert!(success)
})?;
Ok(())
}

#[test]
fn new_shows_mnemonic() -> Result<()> {
testcfg::setup(ForcWalletState::NotInitialized, &|cfg| {
let output = cfg.exec(
&["--path", &format!("{}", cfg.wallet_path.display()), "new"],
&|| {
thread::sleep(Duration::from_millis(1000));
let mut enigo = Enigo::new(&Settings::default()).unwrap();
// First password
enter_password(&mut enigo).unwrap();
// Verify password
thread::sleep(Duration::from_millis(100));
enter_password(&mut enigo).unwrap();
},
);

let expected = "Wallet mnemonic phrase:";
let output_stdout = output.stdout;
dbg!(&output_stdout);
let success = output_stdout.contains(expected);
assert!(success)
})?;
Ok(())
}
Loading
Loading