diff --git a/.github/workflows/general.yml b/.github/workflows/general.yml index 501163c2..21e882cc 100644 --- a/.github/workflows/general.yml +++ b/.github/workflows/general.yml @@ -24,6 +24,8 @@ jobs: tests: name: cargo-test runs-on: ubuntu-latest + env: + CI: true steps: - uses: actions/checkout@v3 - uses: actions-rs/toolchain@v1 diff --git a/.gitignore b/.gitignore index b7be93ce..4b49e731 100644 --- a/.gitignore +++ b/.gitignore @@ -11,8 +11,8 @@ signed.tx # testing files *.feature -test-harness -tests/features +/test-harness +/tests/features # editor *.code-workspace diff --git a/Cargo.toml b/Cargo.toml index 94616e12..228ff292 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,12 +52,19 @@ gloo-timers = { version = "0.2.4", features = ["futures"] } instant = { version = "0.1", features = ["now", "wasm-bindgen"] } [dev-dependencies] +async-trait = "0.1.51" +cucumber = "0.19.0" dotenv = "0.15.0" -tokio = { version = "1.6.0", features = ["rt-multi-thread", "macros"] } -rand = "0.8.3" getrandom = { version = "0.2.2", features = ["js"] } -cucumber = "0.19.0" -async-trait = "0.1.51" +insta = { version = "1.29.0", features = ["yaml"] } +rand = "0.8.3" +tokio = { version = "1.6.0", features = ["rt-multi-thread", "macros"] } + +[profile.dev.package.insta] +opt-level = 3 + +[profile.dev.package.similar] +opt-level = 3 [features] default = ["native"] @@ -65,7 +72,7 @@ native = ["algonaut_kmd/native"] rustls = ["algonaut_kmd/rustls"] [[test]] -name = "features_runner" +name = "cucumber" # Allows Cucumber to print output instead of libtest harness = false test = false diff --git a/Makefile b/Makefile index b311c373..d576a100 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ integration: - cargo test --test features_runner -- + cargo test --test cucumber -- -vv harness: ./test-harness.sh up diff --git a/algonaut_transaction/src/api_model.rs b/algonaut_transaction/src/api_model.rs index a9210f69..16a454ce 100644 --- a/algonaut_transaction/src/api_model.rs +++ b/algonaut_transaction/src/api_model.rs @@ -861,4 +861,4 @@ mod tests { .is_err() ); } -} \ No newline at end of file +} diff --git a/src/constant.rs b/src/constant.rs new file mode 100644 index 00000000..dbae206e --- /dev/null +++ b/src/constant.rs @@ -0,0 +1,5 @@ +/// Application ID prefix when signing +pub const APPID_PREFIX: &[u8; 5] = b"appID"; + +/// how long addresses are in bytes +pub const KEN_LEN_BYTES: u64 = 32; diff --git a/src/lib.rs b/src/lib.rs index 1662cebf..46971f5e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,8 +21,11 @@ pub mod indexer; pub mod kmd; pub mod atomic_transaction_composer; +pub mod constant; pub mod error; pub use error::Error; +pub mod logic; + pub mod util; diff --git a/src/logic.rs b/src/logic.rs new file mode 100644 index 00000000..6b579ab3 --- /dev/null +++ b/src/logic.rs @@ -0,0 +1,27 @@ +use crate::constant::APPID_PREFIX; +use sha2::{Digest, Sha512}; + +pub fn get_application_address(app_id: u64) -> Vec { + let app_id: &[u8; 8] = &app_id.to_be_bytes(); + let to_sign: Vec = APPID_PREFIX + .iter() + .copied() + .chain(app_id.iter().copied()) + .collect(); + + let mut hasher = Sha512::new(); + hasher.update(to_sign); + + hasher.finalize()[..].to_vec() +} + +#[cfg(test)] +mod tests { + use super::get_application_address; + + #[test] + fn get_application_address_snapshots() { + let a = get_application_address(13); + insta::assert_yaml_snapshot!(a); + } +} diff --git a/src/snapshots/algonaut__logic__tests__get_application_address_snapshots.snap b/src/snapshots/algonaut__logic__tests__get_application_address_snapshots.snap new file mode 100644 index 00000000..ebbf8ac8 --- /dev/null +++ b/src/snapshots/algonaut__logic__tests__get_application_address_snapshots.snap @@ -0,0 +1,69 @@ +--- +source: src/logic.rs +expression: a +--- +- 174 +- 108 +- 145 +- 15 +- 43 +- 99 +- 36 +- 10 +- 139 +- 82 +- 201 +- 202 +- 142 +- 242 +- 99 +- 74 +- 61 +- 193 +- 187 +- 44 +- 151 +- 217 +- 248 +- 206 +- 180 +- 209 +- 90 +- 215 +- 232 +- 66 +- 175 +- 204 +- 169 +- 225 +- 82 +- 128 +- 30 +- 94 +- 110 +- 70 +- 3 +- 59 +- 48 +- 108 +- 176 +- 169 +- 159 +- 62 +- 187 +- 88 +- 65 +- 125 +- 161 +- 12 +- 43 +- 23 +- 172 +- 246 +- 74 +- 228 +- 121 +- 195 +- 177 +- 111 + diff --git a/tests/cucumber.rs b/tests/cucumber.rs new file mode 100644 index 00000000..805ab4d1 --- /dev/null +++ b/tests/cucumber.rs @@ -0,0 +1,33 @@ +use cucumber::World; +use step_defs::world; + +mod step_defs; + +#[tokio::main] +async fn main() { + // NOTE: we don't support algod v1 anymore + // features which depend completely on algod v1 are omitted + + // algod feature: omitted (algod v1) + // assets feature: omitted (algod v1) + + // TODO use tags - so we don't have to create a new config per file (until the tests are complete) + + world::World::cucumber() + .max_concurrent_scenarios(1) + .fail_on_skipped() + .run_and_exit("tests/features/integration/applications.feature") + .await; + + world::World::cucumber() + .max_concurrent_scenarios(1) + .fail_on_skipped() + .run_and_exit("tests/features/integration/abi.feature") + .await; + + world::World::cucumber() + .max_concurrent_scenarios(1) + .fail_on_skipped() + .run_and_exit("tests/features/integration/c2c.feature") + .await; +} diff --git a/tests/features_runner.rs b/tests/features_runner.rs deleted file mode 100644 index b232e352..00000000 --- a/tests/features_runner.rs +++ /dev/null @@ -1,36 +0,0 @@ -use cucumber::World; -use step_defs::integration; - -mod step_defs; - -#[tokio::main] -async fn main() { - // NOTE: we don't support algod v1 anymore - // features which depend completely on algod v1 are omitted - - // algod feature: omitted (algod v1) - // assets feature: omitted (algod v1) - - // TODO use tags - so we don't have to create a new config per file (until the tests are complete) - - integration::world::World::cucumber() - .max_concurrent_scenarios(1) - // show output (e.g. println! or dbg!) in terminal https://cucumber-rs.github.io/cucumber/current/output/terminal.html#manual-printing - // .with_writer( - // writer::Basic::raw(io::stdout(), writer::Coloring::Auto, 0) - // .summarized() - // .assert_normalized(), - // ) - .run("tests/features/integration/applications.feature") - .await; - - integration::world::World::cucumber() - .max_concurrent_scenarios(1) - .run("tests/features/integration/abi.feature") - .await; - - integration::world::World::cucumber() - .max_concurrent_scenarios(1) - .run("tests/features/integration/c2c.feature") - .await; -} diff --git a/tests/step_defs/integration/abi.rs b/tests/step_defs/abi.rs similarity index 99% rename from tests/step_defs/integration/abi.rs rename to tests/step_defs/abi.rs index 79b0d377..e89b64b5 100644 --- a/tests/step_defs/integration/abi.rs +++ b/tests/step_defs/abi.rs @@ -1,6 +1,6 @@ use crate::step_defs::{ - integration::world::World, util::{read_teal, wait_for_pending_transaction}, + world::World, }; use algonaut::atomic_transaction_composer::{ transaction_signer::TransactionSigner, AbiArgValue, AbiMethodReturnValue, AbiReturnDecodeError, diff --git a/tests/step_defs/account.rs b/tests/step_defs/account.rs new file mode 100644 index 00000000..b3cb86f1 --- /dev/null +++ b/tests/step_defs/account.rs @@ -0,0 +1,10 @@ +use crate::step_defs::world::World; +use cucumber::then; + +#[then( + "I get the account address for the current application and see that it matches the app id's hash" +)] +async fn assert_app_account_is_the_hash(w: &mut World) { + let _app_id = w.app_id; + // TODO +} diff --git a/tests/step_defs/integration/applications.rs b/tests/step_defs/applications.rs similarity index 89% rename from tests/step_defs/integration/applications.rs rename to tests/step_defs/applications.rs index 5f3142d6..709b7b05 100644 --- a/tests/step_defs/integration/applications.rs +++ b/tests/step_defs/applications.rs @@ -1,5 +1,7 @@ -use crate::step_defs::integration::world::World; -use crate::step_defs::util::{parse_app_args, read_teal, split_addresses, split_uint64}; +use crate::step_defs::util::{ + read_teal, split_addresses, split_and_process_app_args, split_uint64, +}; +use crate::step_defs::world::World; use algonaut_algod::models::{Application, ApplicationLocalState}; use algonaut_transaction::builder::{ CallApplication, ClearApplication, CloseApplication, DeleteApplication, OptInApplication, @@ -12,13 +14,13 @@ use data_encoding::BASE64; use std::error::Error; #[given( - regex = r#"^I build an application transaction with the transient account, the current application, suggested params, operation "([^"]*)", approval-program "([^"]*)", clear-program "([^"]*)", global-bytes (\d+), global-ints (\d+), local-bytes (\d+), local-ints (\d+), app-args "([^"]*)", foreign-apps "([^"]*)", foreign-assets "([^"]*)", app-accounts "([^"]*)", extra-pages (\d+)$"# + regex = r#"^I build an application transaction with the transient account, the current application, suggested params, operation "([^"]*)", approval-program "([^"]*)", clear-program "([^"]*)", global-bytes (\d+), global-ints (\d+), local-bytes (\d+), local-ints (\d+), app-args "([^"]*)", foreign-apps "([^"]*)", foreign-assets "([^"]*)", app-accounts "([^"]*)", extra-pages (\d+), boxes "([^"]*)"$"# )] #[then( - regex = r#"^I build an application transaction with the transient account, the current application, suggested params, operation "([^"]*)", approval-program "([^"]*)", clear-program "([^"]*)", global-bytes (\d+), global-ints (\d+), local-bytes (\d+), local-ints (\d+), app-args "([^"]*)", foreign-apps "([^"]*)", foreign-assets "([^"]*)", app-accounts "([^"]*)", extra-pages (\d+)$"# + regex = r#"^I build an application transaction with the transient account, the current application, suggested params, operation "([^"]*)", approval-program "([^"]*)", clear-program "([^"]*)", global-bytes (\d+), global-ints (\d+), local-bytes (\d+), local-ints (\d+), app-args "([^"]*)", foreign-apps "([^"]*)", foreign-assets "([^"]*)", app-accounts "([^"]*)", extra-pages (\d+), boxes "([^"]*)"$"# )] #[when( - regex = r#"^I build an application transaction with the transient account, the current application, suggested params, operation "([^"]*)", approval-program "([^"]*)", clear-program "([^"]*)", global-bytes (\d+), global-ints (\d+), local-bytes (\d+), local-ints (\d+), app-args "([^"]*)", foreign-apps "([^"]*)", foreign-assets "([^"]*)", app-accounts "([^"]*)", extra-pages (\d+)$"# + regex = r#"^I build an application transaction with the transient account, the current application, suggested params, operation "([^"]*)", approval-program "([^"]*)", clear-program "([^"]*)", global-bytes (\d+), global-ints (\d+), local-bytes (\d+), local-ints (\d+), app-args "([^"]*)", foreign-apps "([^"]*)", foreign-assets "([^"]*)", app-accounts "([^"]*)", extra-pages (\d+), boxes "([^"]*)"$"# )] async fn i_build_an_application_transaction( w: &mut World, @@ -34,11 +36,12 @@ async fn i_build_an_application_transaction( foreign_assets: String, app_accounts: String, extra_pages: u32, + _boxes: String, // TODO implement boxes ) -> Result<(), Box> { let algod = w.algod.as_ref().unwrap(); let transient_account = w.transient_account.as_ref().unwrap(); - let args = parse_app_args(app_args)?; + let args = split_and_process_app_args(app_args); let accounts = split_addresses(app_accounts)?; @@ -283,3 +286,28 @@ async fn the_transient_account_should_have( Ok(()) } + +#[then( + regex = r#"according to "([^"]*)", the contents of the box with name "([^"]*)" in the current application should be "([^"]*)". If there is an error it is "([^"]*)"."# +)] +async fn check_box_contents( + _w: &mut World, + _context: String, + _from_client: String, + box_name: String, + _box_value: String, + _error_string: String, +) { + let _box_name = split_and_process_app_args(box_name); + + // TODO + // if from_client == "algod" { + // let box_response = w + // .algod + // .as_ref() + // .unwrap() + // .app_box(w.app_id.unwrap(), &box_name) + // .await + // .ok(); + // } +} diff --git a/tests/step_defs/integration/general.rs b/tests/step_defs/general.rs similarity index 93% rename from tests/step_defs/integration/general.rs rename to tests/step_defs/general.rs index ac99cdfa..454b27f7 100644 --- a/tests/step_defs/integration/general.rs +++ b/tests/step_defs/general.rs @@ -1,15 +1,15 @@ use crate::step_defs::{ - integration::world::World, util::{account_from_kmd_response, wait_for_pending_transaction}, + world::World, }; -use algonaut::{algod::v2::Algod, kmd::v1::Kmd}; +use algonaut::{algod::v2::Algod, indexer::v2::Indexer, kmd::v1::Kmd}; use algonaut_core::MicroAlgos; use algonaut_transaction::{Pay, TxnBuilder}; use cucumber::{given, then, when}; use rand::Rng; use std::error::Error; -#[given(regex = "an algod v2 client")] +#[given("an algod v2 client")] async fn an_algod_v2_client(w: &mut World) -> Result<(), Box> { let algod = Algod::new( "http://localhost:60000", @@ -23,6 +23,15 @@ async fn an_algod_v2_client(w: &mut World) -> Result<(), Box> { Ok(()) } +#[given("an indexer v2 client")] +async fn an_indexer_v2_client(w: &mut World) -> Result<(), Box> { + let indexer = Indexer::new("http://localhost:59999", "").unwrap(); + + w.indexer = Some(indexer); + + Ok(()) +} + #[given(regex = r#"^an algod v2 client connected to "([^"]*)" port (\d+) with token "([^"]*)"$"#)] async fn an_algod_v2_client_connected_to(w: &mut World, host: String, port: String, token: String) { let algod = Algod::new(&format!("http://{}:{}", host, port), &token).unwrap(); diff --git a/tests/step_defs/integration/mod.rs b/tests/step_defs/integration/mod.rs deleted file mode 100644 index 8403db21..00000000 --- a/tests/step_defs/integration/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod abi; -pub mod applications; -pub mod general; -pub mod world; diff --git a/tests/step_defs/mod.rs b/tests/step_defs/mod.rs index 299ac737..5f683ba5 100644 --- a/tests/step_defs/mod.rs +++ b/tests/step_defs/mod.rs @@ -1,2 +1,6 @@ -pub mod integration; -mod util; +pub mod abi; +pub mod account; +pub mod applications; +pub mod general; +pub mod util; +pub mod world; diff --git a/tests/step_defs/util.rs b/tests/step_defs/util.rs index 8cc463e5..03e9bbac 100644 --- a/tests/step_defs/util.rs +++ b/tests/step_defs/util.rs @@ -1,3 +1,9 @@ +use algonaut::algod::v2::Algod; +use algonaut_algod::models::PendingTransactionResponse; +use algonaut_core::{Address, CompiledTeal}; +use algonaut_model::kmd::v1::ExportKeyResponse; +use algonaut_transaction::account::Account; +use std::str::FromStr; use std::{ convert::TryInto, error::Error, @@ -6,12 +12,6 @@ use std::{ time::{Duration, Instant}, }; -use algonaut::algod::v2::Algod; -use algonaut_algod::models::PendingTransactionResponse; -use algonaut_core::{Address, CompiledTeal}; -use algonaut_model::kmd::v1::ExportKeyResponse; -use algonaut_transaction::account::Account; - /// Utility function to wait on a transaction to be confirmed pub async fn wait_for_pending_transaction( algod: &Algod, @@ -45,33 +45,6 @@ pub fn split_addresses(args_str: String) -> Result, String> { args_str.split(",").map(|a| a.parse()).collect() } -pub fn parse_app_args(args_str: String) -> Result>, Box> { - if args_str.is_empty() { - return Ok(vec![]); - } - - let args = args_str.split(","); - - let mut args_bytes: Vec> = vec![]; - for arg in args { - let parts = arg.split(":").collect::>(); - let type_part = parts[0]; - match type_part { - "str" => args_bytes.push(parts[1].as_bytes().to_vec()), - "int" => { - let int = parts[1].parse::()?; - args_bytes.push(int.to_be_bytes().to_vec()); - } - _ => Err(format!( - "Applications doesn't currently support argument of type {}", - type_part - ))?, - } - } - - Ok(args_bytes) -} - pub fn account_from_kmd_response(key_res: &ExportKeyResponse) -> Result> { Ok(Account::from_seed(key_res.private_key[0..32].try_into()?)) } @@ -85,3 +58,54 @@ pub async fn read_teal(algod: &Algod, file_name: &str) -> CompiledTeal { CompiledTeal(file_bytes) } } + +pub fn split_and_process_app_args(s: String) -> Vec> { + s.split(',') + .filter(|s| !s.is_empty()) + .map(|arg| arg.parse::().unwrap().as_bytes()) + .collect() +} + +#[derive(PartialEq, Debug)] +pub enum AppArg { + Int(u64), + Str(String), + B64(String), + Addr(String), +} + +impl AppArg { + pub fn as_bytes(&self) -> Vec { + match self { + Self::Int(a) => a.to_be_bytes().to_vec(), + Self::Str(s) => s.as_bytes().to_vec(), + Self::B64(s) => s.as_bytes().to_vec(), // TODO + Self::Addr(s) => s.as_bytes().to_vec(), // TODO + } + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct ParseAppArgError; + +impl FromStr for AppArg { + type Err = ParseAppArgError; + + /// Takes in a tuple where first element is the encoding and second element is value. + /// If there is only one element, then it is assumed to be an int. + fn from_str(s: &str) -> Result { + let sub_args: Vec = s.to_owned().split(':').map(|s| s.to_owned()).collect(); + + let (l, r) = (sub_args[0].clone(), sub_args.get(1).cloned()); + + if l == "str" { + Ok(Self::Str(r.unwrap())) + } else if l == "b64" { + Ok(Self::B64(r.unwrap())) + } else if l == "addr" { + Ok(Self::Addr(r.unwrap())) + } else { + Ok(Self::Int(l.parse().unwrap())) + } + } +} diff --git a/tests/step_defs/integration/world.rs b/tests/step_defs/world.rs similarity index 96% rename from tests/step_defs/integration/world.rs rename to tests/step_defs/world.rs index 9d035791..50619f91 100644 --- a/tests/step_defs/integration/world.rs +++ b/tests/step_defs/world.rs @@ -4,6 +4,7 @@ use algonaut::{ atomic_transaction_composer::{ transaction_signer::TransactionSigner, AbiArgValue, ExecuteResult, TransactionWithSigner, }, + indexer::v2::Indexer, kmd::v1::Kmd, }; use algonaut_abi::{abi_interactions::AbiMethod, abi_type::AbiType}; @@ -15,6 +16,7 @@ use cucumber; #[derive(Default, Debug, cucumber::World)] pub struct World { pub algod: Option, + pub indexer: Option, pub kmd: Option, pub handle: Option, diff --git a/tests/test_step_defs.rs b/tests/test_step_defs.rs new file mode 100644 index 00000000..ea9d2c28 --- /dev/null +++ b/tests/test_step_defs.rs @@ -0,0 +1,11 @@ +mod step_defs; + +use step_defs::util::AppArg; + +#[test] +fn app_args() { + assert_eq!(AppArg::Int(0), "0".parse().unwrap()); + assert_eq!(AppArg::Str("name".to_owned()), "str:name".parse().unwrap()); + assert_eq!(AppArg::B64("aaa".to_owned()), "b64:aaa".parse().unwrap()); + assert_eq!(AppArg::Addr("AAA".to_owned()), "addr:AAA".parse().unwrap()); +}