use cucumber::{given, then, when, World}; use futures::future::BoxFuture; use std::{ collections::HashMap, path::{Path, PathBuf}, sync::Arc, }; use tokio::{fs, io}; use verax::{storage::SQLite, AccountId, Amount, Asset, RevId, TokenPayload}; #[derive(Debug, Clone)] pub enum Variable { RevId(RevId), RevIdResult(Result), } impl From for Variable { fn from(rev_id: RevId) -> Self { Variable::RevId(rev_id) } } impl From> for Variable { fn from(rev_id: Result) -> Self { Variable::RevIdResult(rev_id.map_err(|e| e.to_string())) } } #[derive(Debug, World)] pub struct LedgerWorld { ledger: Arc>, variables: HashMap, spend: Option>, tokens: Vec, receive: Option>, } impl Default for LedgerWorld { fn default() -> Self { futures::executor::block_on(async move { let settings = "sqlite://:memory:" .parse::() .expect("valid settings") .journal_mode(verax::storage::sqlite::SqliteJournalMode::Wal) .create_if_missing(true); let pool = verax::storage::sqlite::SqlitePoolOptions::new() .connect_with(settings) .await .expect("pool"); let db = SQLite::new(pool); db.setup().await.expect("setup"); Self { ledger: verax::Ledger::new(db.into()), variables: HashMap::new(), tokens: vec![], spend: None, receive: None, } }) } } impl LedgerWorld { pub fn get(&self, name: &str) -> &Variable { self.variables.get(name).as_ref().expect("variable") } } #[given("a new transaction")] fn new_transaction(world: &mut LedgerWorld) { world.spend = Some(vec![]); world.receive = Some(vec![]); } #[given(expr = "spend {word} {word} from {word}")] fn spend(world: &mut LedgerWorld, amount: String, asset: String, account: String) { let asset = asset.parse::().expect("valid asset"); let amount = asset.from_human(&amount).expect("valid amount"); world .spend .as_mut() .expect("has spend") .push((account.parse().expect("valid account"), amount)); } #[then(expr = "withdraw from {word} {word} {word}")] async fn withdraw(world: &mut LedgerWorld, account: String, amount: String, asset: String) { let asset = asset.parse::().expect("valid asset"); let amount = asset.from_human(&amount).expect("valid amount"); let account = account.parse().expect("valid account"); world.variables.insert( "latest".to_owned(), world .ledger .withdrawal(&account, amount, "settled".into(), "test".to_owned(), None) .await .map(|x| x.revision_id) .into(), ); } #[then("it fails")] async fn it_fails(world: &mut LedgerWorld) { match world.variables.get("latest").expect("latest") { Variable::RevIdResult(Err(_)) => {} latest => panic!("expected error found: {:?}", latest), } } #[given(expr = "receive {word} {word} in {word}")] fn receive(world: &mut LedgerWorld, amount: String, asset: String, account: String) { let asset = asset.parse::().expect("valid asset"); let amount = asset.from_human(&amount).expect("valid amount"); world .receive .as_mut() .expect("has receive") .push((account.parse().expect("valid account"), amount)); } #[when(expr = "commit transaction {word} as {word}")] async fn commit_transaction(world: &mut LedgerWorld, name: String, status: String) { let status = status.parse::().expect("valid status"); let spend = world.spend.take().expect("has spend"); let receive = world.receive.take().expect("has receive"); world.variables.insert( name, world .ledger .new_transaction("Transaction".to_owned(), status, spend, receive, None) .await .map(|x| x.revision_id) .into(), ); } #[given(expr = "a {word} deposit {word} of {word} {word} for {word}")] async fn deposit_funds( world: &mut LedgerWorld, status: String, name: String, amount: String, asset: String, account: String, ) { let asset = asset.parse::().expect("valid asset"); world.variables.insert( name, world .ledger .deposit( &account.parse().expect("valid account"), asset.from_human(&amount).expect("valid amount"), status.parse().expect("valid status"), vec![], "Initial deposit".to_owned(), None, ) .await .map(|x| x.revision_id) .into(), ); } #[given(expr = "update {word} set status to {word}")] async fn update_status_from_last_tx(world: &mut LedgerWorld, tx: String, new_status: String) { let revision = match world.get(&tx) { Variable::RevId(rev_id) => rev_id.clone(), Variable::RevIdResult(Ok(rev_id)) => rev_id.clone(), _ => panic!("{} is not a RevId", tx), }; let status = new_status.parse().expect("valid status"); let update_token = world.tokens.pop(); world.variables.insert( tx, world .ledger .change_status(revision, status, "update status".to_owned(), update_token) .await .map(|x| x.revision_id) .into(), ); } #[then(expr = "{word} has failed")] fn has_failed(world: &mut LedgerWorld, tx: String) { assert!(matches!( world.get(&tx).clone(), Variable::RevIdResult(Err(_)) )); } #[then(expr = "{word} has no balance")] async fn check_balance_empty(world: &mut LedgerWorld, account: String) { let balance = world .ledger .get_balance(&account.parse().expect("valid account")) .await .expect("balance") .into_iter() .filter(|b| b.cents() != 0) .collect::>(); assert_eq!(0, balance.len()) } #[then(expr = "{word} has balance of {word} {word}")] async fn check_balance(world: &mut LedgerWorld, account: String, amount: String, asset: String) { let asset = asset.parse::().expect("valid asset"); let amount = asset.from_human(&amount).expect("valid amount"); let balances = world .ledger .get_balance(&account.parse().expect("valid account")) .await .expect("balance") .into_iter() .filter(|b| b.asset() == &asset) .collect::>(); assert_eq!(1, balances.len(), "{} is found", asset); assert_eq!(balances.get(0), Some(&amount)); } fn find_features<'a, A: AsRef + Send + Sync>( dir_path: &'a A, ) -> BoxFuture<'a, io::Result>> { Box::pin(async move { let mut entries = fs::read_dir(dir_path).await?; let mut paths = vec![]; while let Ok(Some(entry)) = entries.next_entry().await { let path = entry.path(); if path.is_dir() { paths.extend(find_features(&path).await?); } else if let Some(extension) = path.extension() { if extension == "feature" { paths.push(path); } } } Ok(paths) }) } #[tokio::main] async fn main() { for file in find_features(&".").await.expect("files") { LedgerWorld::run(file).await; } }