From a4db7f4c0d1f1022ccdae99ef7e2f8797ec72a5e Mon Sep 17 00:00:00 2001 From: Ivan Schuetz Date: Mon, 6 Dec 2021 18:46:13 +0100 Subject: [PATCH 1/9] Make clients derive Debug --- algonaut_client/src/algod/v2/mod.rs | 1 + algonaut_client/src/indexer/v2/mod.rs | 1 + algonaut_client/src/kmd/v1/mod.rs | 1 + src/algod/v2/mod.rs | 1 + src/indexer/v2/mod.rs | 1 + src/kmd/v1/mod.rs | 1 + 6 files changed, 6 insertions(+) diff --git a/algonaut_client/src/algod/v2/mod.rs b/algonaut_client/src/algod/v2/mod.rs index 84b070f6..e4fb66c3 100644 --- a/algonaut_client/src/algod/v2/mod.rs +++ b/algonaut_client/src/algod/v2/mod.rs @@ -10,6 +10,7 @@ use algonaut_model::algod::v2::{ use reqwest::header::HeaderMap; use reqwest::Url; +#[derive(Debug)] /// Client for interacting with the Algorand protocol daemon pub struct Client { url: String, diff --git a/algonaut_client/src/indexer/v2/mod.rs b/algonaut_client/src/indexer/v2/mod.rs index f3b57ed9..680263fe 100644 --- a/algonaut_client/src/indexer/v2/mod.rs +++ b/algonaut_client/src/indexer/v2/mod.rs @@ -13,6 +13,7 @@ use reqwest::header::HeaderMap; use reqwest::Url; /// Client interacting with the Algorand's indexer +#[derive(Debug)] pub struct Client { pub(super) url: String, pub(super) headers: HeaderMap, diff --git a/algonaut_client/src/kmd/v1/mod.rs b/algonaut_client/src/kmd/v1/mod.rs index 5e6a6f26..27de43ee 100644 --- a/algonaut_client/src/kmd/v1/mod.rs +++ b/algonaut_client/src/kmd/v1/mod.rs @@ -19,6 +19,7 @@ use algonaut_model::kmd::v1::{ use reqwest::header::HeaderMap; use reqwest::Url; +#[derive(Debug)] /// Client for interacting with the key management daemon pub struct Client { pub(super) address: String, diff --git a/src/algod/v2/mod.rs b/src/algod/v2/mod.rs index e35d1934..dfce8572 100644 --- a/src/algod/v2/mod.rs +++ b/src/algod/v2/mod.rs @@ -9,6 +9,7 @@ use algonaut_transaction::SignedTransaction; use crate::error::AlgonautError; +#[derive(Debug)] pub struct Algod { pub(crate) client: Client, } diff --git a/src/indexer/v2/mod.rs b/src/indexer/v2/mod.rs index 86ae1c59..430f9323 100644 --- a/src/indexer/v2/mod.rs +++ b/src/indexer/v2/mod.rs @@ -10,6 +10,7 @@ use algonaut_model::indexer::v2::{ use crate::error::AlgonautError; +#[derive(Debug)] pub struct Indexer { pub(super) client: Client, } diff --git a/src/kmd/v1/mod.rs b/src/kmd/v1/mod.rs index 1fdefa32..9517bebe 100644 --- a/src/kmd/v1/mod.rs +++ b/src/kmd/v1/mod.rs @@ -13,6 +13,7 @@ use algonaut_transaction::Transaction; use crate::error::AlgonautError; +#[derive(Debug)] pub struct Kmd { pub(crate) client: Client, } From 53c2b3dd3ba94c5c831344bf051e0665aede1a12 Mon Sep 17 00:00:00 2001 From: Ivan Schuetz Date: Mon, 6 Dec 2021 19:11:42 +0100 Subject: [PATCH 2/9] Make txn optional --- algonaut_model/src/algod/v2/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/algonaut_model/src/algod/v2/mod.rs b/algonaut_model/src/algod/v2/mod.rs index 41d0dfb0..d715f705 100644 --- a/algonaut_model/src/algod/v2/mod.rs +++ b/algonaut_model/src/algod/v2/mod.rs @@ -704,7 +704,7 @@ pub struct BlockHeader { pub rwd: String, pub seed: String, pub ts: u64, - pub txn: String, + pub txn: Option, } /// Catchup From 863679165ea7e2246a20ffdb83d875fdb88144e0 Mon Sep 17 00:00:00 2001 From: Ivan Schuetz Date: Mon, 6 Dec 2021 19:27:26 +0100 Subject: [PATCH 3/9] Configure tests Temporary step defs --- .gitignore | 4 +++ Cargo.toml | 8 +++++ Makefile | 5 +++ tests/cucumber.rs | 67 ++++++++++++++++++++++++++++++++++++++ tests/docker/Dockerfile | 13 ++++++++ tests/docker/run_docker.sh | 28 ++++++++++++++++ 6 files changed, 125 insertions(+) create mode 100644 Makefile create mode 100644 tests/cucumber.rs create mode 100644 tests/docker/Dockerfile create mode 100755 tests/docker/run_docker.sh diff --git a/.gitignore b/.gitignore index 20228899..4eb2afb2 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,7 @@ Cargo.lock /test.sh signed.tx **/.env + +# ignore cucumber test resources +test-harness/ +tests/features/ diff --git a/Cargo.toml b/Cargo.toml index f9e27645..c2284cef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,8 +34,16 @@ tokio = { version = "1.6.0", features = ["rt-multi-thread", "macros"] } rand = "0.8.3" getrandom = { version = "0.2.2", features = ["js"] } data-encoding = "2.3.1" +# Using main branch because of this issue: https://github.com/cucumber-rs/cucumber/issues/173 +# TODO replace with v0.11 once released ("few days" from now, according to maintainer) +cucumber = { git = "https://github.com/cucumber-rs/cucumber.git" } +async-trait = "0.1.51" [features] default = ["native"] native = ["algonaut_client/native"] rustls = ["algonaut_client/rustls"] + +[[test]] +name = "cucumber" +harness = false # Allows Cucumber to print output instead of libtest diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..19c9600d --- /dev/null +++ b/Makefile @@ -0,0 +1,5 @@ +integration: + cargo test --test cucumber -- + +docker-test: + ./tests/docker/run_docker.sh diff --git a/tests/cucumber.rs b/tests/cucumber.rs new file mode 100644 index 00000000..494824cf --- /dev/null +++ b/tests/cucumber.rs @@ -0,0 +1,67 @@ +use algonaut::algod::v2::Algod; +use algonaut::algod::AlgodBuilder; +use algonaut_core::Round; +use algonaut_model::algod::v2::NodeStatus; +use async_trait::async_trait; +use cucumber::{given, then, when, WorldInit}; +use std::convert::Infallible; + +#[derive(Default, Debug, WorldInit)] +pub struct World { + algod_client: Option, + node_status: Option, + last_round: Option, +} + +#[async_trait(?Send)] +impl cucumber::World for World { + type Error = Infallible; + + async fn new() -> Result { + Ok(Self::default()) + } +} + +#[given(expr = "an algod client")] +async fn an_algod_client(w: &mut World) { + let algod = AlgodBuilder::new() + .bind("http://localhost:60000") + .auth("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + .build_v2() + .unwrap(); + + w.algod_client = Some(algod) +} + +#[then(expr = "the node should be healthy")] +async fn node_is_healthy(w: &mut World) { + let algod_client = w.algod_client.as_ref().unwrap(); + algod_client.health().await.unwrap(); +} + +#[when(expr = "I get the status")] +async fn i_get_the_status(w: &mut World) { + let algod_client = w.algod_client.as_ref().unwrap(); + let status = algod_client.status().await.unwrap(); + w.last_round = Some(status.last_round); + w.node_status = Some(status); +} + +#[when(expr = "I get status after this block")] +async fn i_get_the_status_after_this_block(w: &mut World) { + let algod_client = w.algod_client.as_ref().unwrap(); + let block = w.last_round.unwrap(); + algod_client.status_after_round(Round(block)).await.unwrap(); +} + +#[then(expr = "I can get the block info")] +async fn i_can_get_the_block_info(w: &mut World) { + let algod_client = w.algod_client.as_ref().unwrap(); + let last_round = w.last_round.unwrap(); + algod_client.block(Round(last_round)).await.unwrap(); +} + +#[tokio::main] +async fn main() { + World::run("tests/features/integration").await; +} diff --git a/tests/docker/Dockerfile b/tests/docker/Dockerfile new file mode 100644 index 00000000..50faeaca --- /dev/null +++ b/tests/docker/Dockerfile @@ -0,0 +1,13 @@ +# ARG GO_IMAGE=golang:1.13-stretch +# FROM $GO_IMAGE +FROM rust:1.57.0 +RUN apt-get update && apt-get install -y make + +# Copy SDK code into the container +RUN mkdir -p $HOME/algonaut +COPY . $HOME/algonaut +WORKDIR $HOME/algonaut + +# Run integration tests +# CMD ["/bin/bash", "-c", "make unit && make integration"] +CMD ["/bin/bash", "-c", "make integration"] diff --git a/tests/docker/run_docker.sh b/tests/docker/run_docker.sh new file mode 100755 index 00000000..6dd763c4 --- /dev/null +++ b/tests/docker/run_docker.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +set -e + +# reset test harness +rm -rf test-harness +rm -rf tests/features +# fork with modified features, as cucumber-rs doesn't understand some syntax: +# https://github.com/cucumber-rs/cucumber/issues/174 +# https://github.com/cucumber-rs/cucumber/issues/175 +# git clone --single-branch --branch master https://github.com/algorand/algorand-sdk-testing.git test-harness +git clone --single-branch --branch master https://github.com/ivanschuetz/algorand-sdk-testing.git test-harness +# copy feature files into project +mv test-harness/features tests/features + +RUST_IMAGE=rust:1.57.0 + +echo "Building docker image from base \"$RUST_IMAGE\"" + +#build test environment +docker build -t rust-sdk-testing -f tests/docker/Dockerfile "$(pwd)" + +# Start test harness environment +./test-harness/scripts/up.sh -p + +docker run -it \ + --network host \ + rust-sdk-testing:latest From 53e9810c4ad35c5cc0eae70ca18f441a5f67be75 Mon Sep 17 00:00:00 2001 From: Ivan Schuetz Date: Mon, 6 Dec 2021 20:28:35 +0100 Subject: [PATCH 4/9] Github action --- .github/workflows/quickstart.yml | 7 +++++++ tests/docker/run_docker.sh | 4 +--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/quickstart.yml b/.github/workflows/quickstart.yml index e8c80b2a..e894775b 100644 --- a/.github/workflows/quickstart.yml +++ b/.github/workflows/quickstart.yml @@ -32,6 +32,13 @@ jobs: command: test args: --workspace --lib --examples --test test_account --test test_logic_signature + algorand-sdks-tests: + name: Algorand SDKs tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: make docker-test + fmt: name: Rustfmt runs-on: ubuntu-latest diff --git a/tests/docker/run_docker.sh b/tests/docker/run_docker.sh index 6dd763c4..fddec328 100755 --- a/tests/docker/run_docker.sh +++ b/tests/docker/run_docker.sh @@ -23,6 +23,4 @@ docker build -t rust-sdk-testing -f tests/docker/Dockerfile "$(pwd)" # Start test harness environment ./test-harness/scripts/up.sh -p -docker run -it \ - --network host \ - rust-sdk-testing:latest +docker run --network host rust-sdk-testing:latest From b6e94ad58c6d8c56106a445c439588074c022d26 Mon Sep 17 00:00:00 2001 From: Ivan Schuetz Date: Tue, 7 Dec 2021 08:44:46 +0100 Subject: [PATCH 5/9] Modularize step defs --- Cargo.toml | 2 +- Makefile | 2 +- tests/features_runner.rs | 14 +++++++++ tests/step_defs/integration/abi.rs | 29 +++++++++++++++++++ .../integration/algod.rs} | 5 ---- tests/step_defs/integration/mod.rs | 2 ++ tests/step_defs/mod.rs | 1 + 7 files changed, 48 insertions(+), 7 deletions(-) create mode 100644 tests/features_runner.rs create mode 100644 tests/step_defs/integration/abi.rs rename tests/{cucumber.rs => step_defs/integration/algod.rs} (95%) create mode 100644 tests/step_defs/integration/mod.rs create mode 100644 tests/step_defs/mod.rs diff --git a/Cargo.toml b/Cargo.toml index c2284cef..80752437 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,5 +45,5 @@ native = ["algonaut_client/native"] rustls = ["algonaut_client/rustls"] [[test]] -name = "cucumber" +name = "features_runner" harness = false # Allows Cucumber to print output instead of libtest diff --git a/Makefile b/Makefile index 19c9600d..e96b459e 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ integration: - cargo test --test cucumber -- + cargo test --test features_runner -- docker-test: ./tests/docker/run_docker.sh diff --git a/tests/features_runner.rs b/tests/features_runner.rs new file mode 100644 index 00000000..04d0f960 --- /dev/null +++ b/tests/features_runner.rs @@ -0,0 +1,14 @@ +use cucumber::WorldInit; +use step_defs::integration; + +mod step_defs; + +#[tokio::main] +async fn main() { + integration::algod::World::run(integration_path("algod")).await; + integration::abi::World::run(integration_path("abi")).await; +} + +fn integration_path(feature_name: &str) -> String { + format!("tests/features/integration/{}.feature", feature_name) +} diff --git a/tests/step_defs/integration/abi.rs b/tests/step_defs/integration/abi.rs new file mode 100644 index 00000000..75c07c23 --- /dev/null +++ b/tests/step_defs/integration/abi.rs @@ -0,0 +1,29 @@ +use algonaut::algod::v2::Algod; +use algonaut::algod::AlgodBuilder; +use async_trait::async_trait; +use cucumber::{given, WorldInit}; +use std::convert::Infallible; + +#[derive(Default, Debug, WorldInit)] +pub struct World { + algod: Option, +} + +#[async_trait(?Send)] +impl cucumber::World for World { + type Error = Infallible; + + async fn new() -> Result { + Ok(Self::default()) + } +} + +#[given(expr = "an algod v2 client")] +async fn an_algod_v2_client(w: &mut World) { + let algod = AlgodBuilder::new() + .bind("http://localhost:60000") + .auth("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + .build_v2() + .unwrap(); + w.algod = Some(algod) +} diff --git a/tests/cucumber.rs b/tests/step_defs/integration/algod.rs similarity index 95% rename from tests/cucumber.rs rename to tests/step_defs/integration/algod.rs index 494824cf..8b77ff66 100644 --- a/tests/cucumber.rs +++ b/tests/step_defs/integration/algod.rs @@ -60,8 +60,3 @@ async fn i_can_get_the_block_info(w: &mut World) { let last_round = w.last_round.unwrap(); algod_client.block(Round(last_round)).await.unwrap(); } - -#[tokio::main] -async fn main() { - World::run("tests/features/integration").await; -} diff --git a/tests/step_defs/integration/mod.rs b/tests/step_defs/integration/mod.rs new file mode 100644 index 00000000..f6bb8748 --- /dev/null +++ b/tests/step_defs/integration/mod.rs @@ -0,0 +1,2 @@ +pub mod abi; +pub mod algod; diff --git a/tests/step_defs/mod.rs b/tests/step_defs/mod.rs new file mode 100644 index 00000000..5155b774 --- /dev/null +++ b/tests/step_defs/mod.rs @@ -0,0 +1 @@ +pub mod integration; From 060bafa72eb7b1b2949d7016a6d482c8ef9c4556 Mon Sep 17 00:00:00 2001 From: Ivan Schuetz Date: Tue, 7 Dec 2021 09:11:49 +0100 Subject: [PATCH 6/9] Prevent duplicate flows when pushing to open PRs --- .github/workflows/quickstart.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/quickstart.yml b/.github/workflows/quickstart.yml index e894775b..89ccc22a 100644 --- a/.github/workflows/quickstart.yml +++ b/.github/workflows/quickstart.yml @@ -1,4 +1,7 @@ -on: [push, pull_request] +on: + push: + pull_request: + types: [opened] name: Continuous integration From 957968709e08c2a291b84a178a12d764e0944299 Mon Sep 17 00:00:00 2001 From: Ivan Schuetz Date: Tue, 7 Dec 2021 09:27:53 +0100 Subject: [PATCH 7/9] Move SDKs tests to test job --- .github/workflows/quickstart.yml | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/.github/workflows/quickstart.yml b/.github/workflows/quickstart.yml index 89ccc22a..c1b24086 100644 --- a/.github/workflows/quickstart.yml +++ b/.github/workflows/quickstart.yml @@ -20,7 +20,7 @@ jobs: with: command: check - unit: + tests: name: Tests runs-on: ubuntu-latest steps: @@ -34,14 +34,8 @@ jobs: with: command: test args: --workspace --lib --examples --test test_account --test test_logic_signature - - algorand-sdks-tests: - name: Algorand SDKs tests - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - run: make docker-test - + fmt: name: Rustfmt runs-on: ubuntu-latest From 51dc7b3808f8cc1459bfbdbadc6c40ccc2e74660 Mon Sep 17 00:00:00 2001 From: Ivan Schuetz Date: Wed, 8 Dec 2021 17:42:56 +0100 Subject: [PATCH 8/9] Implement applications feature --- Cargo.toml | 1 + algonaut_client/src/kmd/v1/mod.rs | 4 +- algonaut_transaction/src/account.rs | 1 + algonaut_transaction/src/api_model.rs | 2 +- src/kmd/v1/mod.rs | 4 +- tests/features_runner.rs | 12 +- tests/step_defs/integration/applications.rs | 441 ++++++++++++++++++++ tests/step_defs/integration/mod.rs | 1 + tests/step_defs/mod.rs | 1 + tests/step_defs/util.rs | 75 ++++ 10 files changed, 535 insertions(+), 7 deletions(-) create mode 100644 tests/step_defs/integration/applications.rs create mode 100644 tests/step_defs/util.rs diff --git a/Cargo.toml b/Cargo.toml index 80752437..1bddbeed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ algonaut_encoding = {path = "algonaut_encoding", version = "0.3.0"} algonaut_transaction = {path = "algonaut_transaction", version = "0.3.0"} thiserror = "1.0.23" rmp-serde = "0.15.5" +tokio = { version = "1.6.0", features = ["rt-multi-thread", "macros"] } [dev-dependencies] dotenv = "0.15.0" diff --git a/algonaut_client/src/kmd/v1/mod.rs b/algonaut_client/src/kmd/v1/mod.rs index 27de43ee..ac3182aa 100644 --- a/algonaut_client/src/kmd/v1/mod.rs +++ b/algonaut_client/src/kmd/v1/mod.rs @@ -1,7 +1,7 @@ use crate::extensions::reqwest::ResponseExt; use crate::Headers; use crate::{error::ClientError, extensions::reqwest::to_header_map}; -use algonaut_core::MultisigSignature; +use algonaut_core::{Address, MultisigSignature}; use algonaut_crypto::{Ed25519PublicKey, MasterDerivationKey}; use algonaut_model::kmd::v1::{ CreateWalletRequest, CreateWalletResponse, DeleteKeyRequest, DeleteKeyResponse, @@ -263,7 +263,7 @@ impl Client { &self, wallet_handle: &str, wallet_password: &str, - address: &str, + address: &Address, ) -> Result { let req = ExportKeyRequest { wallet_handle_token: wallet_handle.to_string(), diff --git a/algonaut_transaction/src/account.rs b/algonaut_transaction/src/account.rs index 1e526bb4..e284ed1c 100644 --- a/algonaut_transaction/src/account.rs +++ b/algonaut_transaction/src/account.rs @@ -14,6 +14,7 @@ use rand::Rng; use ring::signature::{Ed25519KeyPair, KeyPair}; use serde::{Deserialize, Serialize}; +#[derive(Debug)] pub struct Account { seed: [u8; 32], address: Address, diff --git a/algonaut_transaction/src/api_model.rs b/algonaut_transaction/src/api_model.rs index 646bd7d7..36a3dc89 100644 --- a/algonaut_transaction/src/api_model.rs +++ b/algonaut_transaction/src/api_model.rs @@ -269,7 +269,7 @@ impl From for ApiTransaction { api_t.app_id = call.app_id.and_then(num_as_api_option); api_t.on_complete = num_as_api_option(application_call_on_complete_to_int(&call.on_complete)); - api_t.accounts = call.accounts.to_owned(); + api_t.accounts = call.accounts.clone().and_then(vec_as_api_option); api_t.approval_program = call .approval_program .to_owned() diff --git a/src/kmd/v1/mod.rs b/src/kmd/v1/mod.rs index 9517bebe..bcc31447 100644 --- a/src/kmd/v1/mod.rs +++ b/src/kmd/v1/mod.rs @@ -1,5 +1,5 @@ use algonaut_client::kmd::v1::Client; -use algonaut_core::{MultisigSignature, ToMsgPack}; +use algonaut_core::{Address, MultisigSignature, ToMsgPack}; use algonaut_crypto::{Ed25519PublicKey, MasterDerivationKey}; use algonaut_model::kmd::v1::{ CreateWalletResponse, DeleteKeyResponse, DeleteMultisigResponse, ExportKeyResponse, @@ -134,7 +134,7 @@ impl Kmd { &self, wallet_handle: &str, wallet_password: &str, - address: &str, + address: &Address, ) -> Result { Ok(self .client diff --git a/tests/features_runner.rs b/tests/features_runner.rs index 04d0f960..86853b90 100644 --- a/tests/features_runner.rs +++ b/tests/features_runner.rs @@ -5,8 +5,16 @@ mod step_defs; #[tokio::main] async fn main() { - integration::algod::World::run(integration_path("algod")).await; - integration::abi::World::run(integration_path("abi")).await; + // algod feature: omitted, this tests v1 and we don't support it anymore + // integration::algod::World::run(integration_path("algod")).await; + + // ABI not supported yet + // integration::abi::World::run(integration_path("abi")).await; + + integration::applications::World::cucumber() + .max_concurrent_scenarios(1) + .run(integration_path("applications")) + .await; } fn integration_path(feature_name: &str) -> String { diff --git a/tests/step_defs/integration/applications.rs b/tests/step_defs/integration/applications.rs new file mode 100644 index 00000000..36bf75fd --- /dev/null +++ b/tests/step_defs/integration/applications.rs @@ -0,0 +1,441 @@ +use crate::step_defs::util::{ + account_from_kmd_response, parse_app_args, split_addresses, split_uint64, + wait_for_pending_transaction, +}; +use algonaut::algod::AlgodBuilder; +use algonaut::kmd::KmdBuilder; +use algonaut::{algod::v2::Algod, kmd::v1::Kmd}; +use algonaut_core::{Address, CompiledTealBytes, MicroAlgos}; +use algonaut_model::algod::v2::{Application, ApplicationLocalState}; +use algonaut_transaction::account::Account; +use algonaut_transaction::builder::{ + CallApplication, ClearApplication, CloseApplication, DeleteApplication, OptInApplication, + UpdateApplication, +}; +use algonaut_transaction::transaction::StateSchema; +use algonaut_transaction::{CreateApplication, Pay, Transaction, TxnBuilder}; +use async_trait::async_trait; +use cucumber::{given, then, WorldInit}; +use data_encoding::BASE64; +use std::convert::Infallible; +use std::error::Error; +use std::fs; + +#[derive(Default, Debug, WorldInit)] +pub struct World { + algod: Option, + + kmd: Option, + handle: Option, + password: Option, + accounts: Option>, + + transient_account: Option, + + tx: Option, + tx_id: Option, + + app_id: Option, +} + +#[async_trait(?Send)] +impl cucumber::World for World { + type Error = Infallible; + + async fn new() -> Result { + Ok(Self::default()) + } +} + +#[given(expr = "an algod client")] +async fn an_algod_client(_: &mut World) { + // do nothing - we don't support v1 +} + +#[given(expr = "a kmd client")] +async fn a_kmd_client(w: &mut World) { + let kmd = KmdBuilder::new() + .bind("http://localhost:60001") + .auth("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + .build_v1() + .unwrap(); + w.kmd = Some(kmd) +} + +#[given(expr = "wallet information")] +async fn wallet_information(w: &mut World) -> Result<(), Box> { + let kmd = w.kmd.as_ref().unwrap(); + + let list_response = kmd.list_wallets().await?; + let wallet_id = match list_response + .wallets + .into_iter() + .find(|wallet| wallet.name == "unencrypted-default-wallet") + { + Some(wallet) => wallet.id, + None => return Err("Wallet not found".into()), + }; + let password = ""; + let init_response = kmd.init_wallet_handle(&wallet_id, "").await?; + + let keys = kmd + .list_keys(init_response.wallet_handle_token.as_ref()) + .await?; + + w.password = Some(password.to_owned()); + w.handle = Some(init_response.wallet_handle_token); + w.accounts = Some( + keys.addresses + .into_iter() + .map(|s| s.parse().unwrap()) + .collect(), + ); + + 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 = AlgodBuilder::new() + .bind(&format!("http://{}:{}", host, port)) + .auth(&token) + .build_v2() + .unwrap(); + + w.algod = Some(algod) +} + +#[given(regex = r#"^I create a new transient account and fund it with (\d+) microalgos\.$"#)] +async fn i_create_a_new_transient_account_and_fund_it_with_microalgos( + w: &mut World, + micro_algos: u64, +) -> Result<(), Box> { + let kmd = w.kmd.as_ref().unwrap(); + let algod = w.algod.as_ref().unwrap(); + let accounts = w.accounts.as_ref().unwrap(); + let password = w.password.as_ref().unwrap(); + let handle = w.handle.as_ref().unwrap(); + + let sender_address = accounts[1]; + + let sender_key = kmd.export_key(handle, password, &sender_address).await?; + + let sender_account = account_from_kmd_response(&sender_key)?; + + let params = algod.suggested_transaction_params().await?; + let tx = TxnBuilder::with( + params, + Pay::new( + accounts[1], + sender_account.address(), + MicroAlgos(micro_algos), + ) + .build(), + ) + .build(); + + let s_tx = sender_account.sign_transaction(&tx)?; + + let send_response = algod.broadcast_signed_transaction(&s_tx).await?; + let _ = wait_for_pending_transaction(&algod, &send_response.tx_id); + + w.transient_account = Some(sender_account); + + Ok(()) +} + +#[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+)$"# +)] +#[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+)$"# +)] +async fn i_build_an_application_transaction( + w: &mut World, + operation: String, + approval_program_file: String, + clear_program_file: String, + global_bytes: u64, + global_ints: u64, + local_bytes: u64, + local_ints: u64, + app_args: String, + foreign_apps: String, + foreign_assets: String, + app_accounts: String, + extra_pages: u64, +) -> 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 accounts = split_addresses(app_accounts)?; + + let foreign_apps = split_uint64(&foreign_apps)?; + let foreign_assets = split_uint64(&foreign_assets)?; + + let params = algod.suggested_transaction_params().await?; + + let tx_type = match operation.as_str() { + "create" => { + let approval_program = load_teal(&approval_program_file)?; + let clear_program = load_teal(&clear_program_file)?; + + let global_schema = StateSchema { + number_ints: global_ints, + number_byteslices: global_bytes, + }; + + let local_schema = StateSchema { + number_ints: local_ints, + number_byteslices: local_bytes, + }; + + CreateApplication::new( + transient_account.address(), + CompiledTealBytes(approval_program), + CompiledTealBytes(clear_program), + global_schema, + local_schema, + ) + .foreign_assets(foreign_assets) + .foreign_apps(foreign_apps) + .accounts(accounts) + .app_arguments(args) + .extra_pages(extra_pages) + .build() + } + "update" => { + let app_id = w.app_id.unwrap(); + + let approval_program = load_teal(&approval_program_file)?; + let clear_program = load_teal(&clear_program_file)?; + + UpdateApplication::new( + transient_account.address(), + app_id, + CompiledTealBytes(approval_program), + CompiledTealBytes(clear_program), + ) + .foreign_assets(foreign_assets) + .foreign_apps(foreign_apps) + .accounts(accounts) + .app_arguments(args) + .build() + } + "call" => { + let app_id = w.app_id.unwrap(); + CallApplication::new(transient_account.address(), app_id) + .foreign_assets(foreign_assets) + .foreign_apps(foreign_apps) + .accounts(accounts) + .app_arguments(args) + .build() + } + "optin" => { + let app_id = w.app_id.unwrap(); + + OptInApplication::new(transient_account.address(), app_id) + .foreign_assets(foreign_assets) + .foreign_apps(foreign_apps) + .accounts(accounts) + .app_arguments(args) + .build() + } + "clear" => { + let app_id = w.app_id.unwrap(); + ClearApplication::new(transient_account.address(), app_id) + .foreign_assets(foreign_assets) + .foreign_apps(foreign_apps) + .accounts(accounts) + .app_arguments(args) + .build() + } + "closeout" => { + let app_id = w.app_id.unwrap(); + CloseApplication::new(transient_account.address(), app_id) + .foreign_assets(foreign_assets) + .foreign_apps(foreign_apps) + .accounts(accounts) + .app_arguments(args) + .build() + } + "delete" => { + let app_id = w.app_id.unwrap(); + DeleteApplication::new(transient_account.address(), app_id) + .foreign_assets(foreign_assets) + .foreign_apps(foreign_apps) + .accounts(accounts) + .app_arguments(args) + .build() + } + + _ => Err(format!("Invalid str: {}", operation))?, + }; + + w.tx = Some(TxnBuilder::with(params, tx_type).build()); + + Ok(()) +} + +#[given( + regex = r#"I sign and submit the transaction, saving the txid\. If there is an error it is "([^"]*)"\.$"# +)] +#[then( + regex = r#"I sign and submit the transaction, saving the txid\. If there is an error it is "([^"]*)"\.$"# +)] +async fn i_sign_and_submit_the_transaction_saving_the_tx_id_if_there_is_an_error_it_is( + w: &mut World, + err: String, +) { + let algod = w.algod.as_ref().unwrap(); + let transient_account = w.transient_account.as_ref().unwrap(); + let tx = w.tx.as_ref().unwrap(); + + let s_tx = transient_account.sign_transaction(&tx).unwrap(); + + match algod.broadcast_signed_transaction(&s_tx).await { + Ok(response) => { + w.tx_id = Some(response.tx_id); + } + Err(e) => { + assert!(e.to_string().contains(&err)); + } + } +} + +#[given(expr = "I wait for the transaction to be confirmed.")] +#[then(expr = "I wait for the transaction to be confirmed.")] +async fn i_wait_for_the_transaction_to_be_confirmed(w: &mut World) { + let algod = w.algod.as_ref().unwrap(); + let tx_id = w.tx_id.as_ref().unwrap(); + + wait_for_pending_transaction(&algod, &tx_id).await.unwrap(); +} + +#[given(expr = "I remember the new application ID.")] +async fn i_remember_the_new_application_id(w: &mut World) { + let algod = w.algod.as_ref().unwrap(); + let tx_id = w.tx_id.as_ref().unwrap(); + + let p_tx = algod.pending_transaction_with_id(tx_id).await.unwrap(); + assert!(p_tx.application_index.is_some()); + + w.app_id = p_tx.application_index; +} + +#[then( + regex = r#"^The transient account should have the created app "([^"]*)" and total schema byte-slices (\d+) and uints (\d+), the application "([^"]*)" state contains key "([^"]*)" with value "([^"]*)"$"# +)] +async fn the_transient_account_should_have( + w: &mut World, + app_created: bool, + byte_slices: u64, + uints: u64, + application_state: String, + key: String, + value: String, +) -> Result<(), Box> { + let algod = w.algod.as_ref().unwrap(); + let transient_account = w.transient_account.as_ref().unwrap(); + let app_id = w.app_id.unwrap(); + + let account_infos = algod + .account_information(&transient_account.address()) + .await + .unwrap(); + + assert!(account_infos.apps_total_schema.is_some()); + let total_schema = account_infos.apps_total_schema.unwrap(); + + assert_eq!(byte_slices, total_schema.num_byte_slice); + assert_eq!(uints, total_schema.num_uint); + + let app_in_account = account_infos.created_apps.iter().any(|a| a.id == app_id); + + match (app_created, app_in_account) { + (true, false) => Err(format!("AppId {} is not found in the account", app_id))?, + (false, true) => { + // If no app was created, we don't expect it to be in the account + Err("AppId is not expected to be in the account")? + } + _ => {} + } + + if key.is_empty() { + return Ok(()); + } + + let key_values = match application_state.to_lowercase().as_ref() { + "local" => { + let local_state = account_infos + .apps_local_state + .iter() + .filter(|s| s.id == app_id) + .collect::>(); + + let len = local_state.len(); + if len == 1 { + local_state[0].key_value.clone() + } else { + Err(format!( + "Expected only one matching local state, found {}", + len + ))? + } + } + "global" => { + let apps = account_infos + .created_apps + .iter() + .filter(|s| s.id == app_id) + .collect::>(); + + let len = apps.len(); + if len == 1 { + apps[0].params.global_state.clone() + } else { + Err(format!("Expected only one matching app, found {}", len))? + } + } + _ => Err(format!("Unknown application state: {}", application_state))?, + }; + + if key_values.is_empty() { + Err("Expected key values length to be greater than 0")? + } + + let mut key_value_found = false; + for key_value in key_values.iter().filter(|kv| kv.key == key) { + if key_value.value.value_type == 1 { + let value_bytes = BASE64.decode(value.as_bytes())?; + if key_value.value.bytes != value_bytes { + Err(format!( + "Value mismatch (bytes): expected: '{:?}', got '{:?}'", + value_bytes, key_value.value.bytes + ))? + } + } else if key_value.value.value_type == 0 { + let int_value = value.parse::()?; + + if key_value.value.uint != int_value { + Err(format!( + "Value mismatch (uint): expected: '{}', got '{}'", + value, key_value.value.uint + ))? + } + } + key_value_found = true; + } + + if !key_value_found { + Err(format!("Couldn't find key: '{}'", key))? + } + + Ok(()) +} + +fn load_teal(file_name: &str) -> Result, Box> { + Ok(fs::read(format!("tests/features/resources/{}", file_name))?) +} diff --git a/tests/step_defs/integration/mod.rs b/tests/step_defs/integration/mod.rs index f6bb8748..4dd12ffb 100644 --- a/tests/step_defs/integration/mod.rs +++ b/tests/step_defs/integration/mod.rs @@ -1,2 +1,3 @@ pub mod abi; pub mod algod; +pub mod applications; diff --git a/tests/step_defs/mod.rs b/tests/step_defs/mod.rs index 5155b774..299ac737 100644 --- a/tests/step_defs/mod.rs +++ b/tests/step_defs/mod.rs @@ -1 +1,2 @@ pub mod integration; +mod util; diff --git a/tests/step_defs/util.rs b/tests/step_defs/util.rs new file mode 100644 index 00000000..e02b954c --- /dev/null +++ b/tests/step_defs/util.rs @@ -0,0 +1,75 @@ +use std::{ + convert::TryInto, + error::Error, + num::ParseIntError, + time::{Duration, Instant}, +}; + +use algonaut::{algod::v2::Algod, error::AlgonautError}; +use algonaut_core::Address; +use algonaut_model::{algod::v2::PendingTransaction, 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, + txid: &str, +) -> Result, AlgonautError> { + let timeout = Duration::from_secs(10); + let start = Instant::now(); + loop { + let pending_transaction = algod.pending_transaction_with_id(txid).await?; + // If the transaction has been confirmed or we time out, exit. + if pending_transaction.confirmed_round.is_some() { + return Ok(Some(pending_transaction)); + } else if start.elapsed() >= timeout { + return Ok(None); + } + std::thread::sleep(Duration::from_millis(250)) + } +} + +pub fn split_uint64(args_str: &str) -> Result, ParseIntError> { + if args_str.is_empty() { + return Ok(vec![]); + } + args_str.split(",").map(|a| a.parse()).collect() +} + +pub fn split_addresses(args_str: String) -> Result, String> { + if args_str.is_empty() { + return Ok(vec![]); + } + 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()?)) +} From 5270f103bbb2d027467f07d79a3d83e528d183e9 Mon Sep 17 00:00:00 2001 From: Ivan Schuetz Date: Wed, 8 Dec 2021 20:22:05 +0100 Subject: [PATCH 9/9] Cleanup, add notes --- tests/features_runner.rs | 11 ++-- tests/step_defs/integration/abi.rs | 29 ---------- tests/step_defs/integration/algod.rs | 62 --------------------- tests/step_defs/integration/applications.rs | 3 +- tests/step_defs/integration/mod.rs | 2 - 5 files changed, 9 insertions(+), 98 deletions(-) delete mode 100644 tests/step_defs/integration/abi.rs delete mode 100644 tests/step_defs/integration/algod.rs diff --git a/tests/features_runner.rs b/tests/features_runner.rs index 86853b90..1770e71c 100644 --- a/tests/features_runner.rs +++ b/tests/features_runner.rs @@ -5,16 +5,19 @@ mod step_defs; #[tokio::main] async fn main() { - // algod feature: omitted, this tests v1 and we don't support it anymore - // integration::algod::World::run(integration_path("algod")).await; + // NOTE: we don't support algod v1 anymore + // features which depend completely on algod v1 are omitted - // ABI not supported yet - // integration::abi::World::run(integration_path("abi")).await; + // algod feature: omitted (algod v1) + + // TODO abi feature: ABI not supported yet integration::applications::World::cucumber() .max_concurrent_scenarios(1) .run(integration_path("applications")) .await; + + // assets feature: omitted (algod v1) } fn integration_path(feature_name: &str) -> String { diff --git a/tests/step_defs/integration/abi.rs b/tests/step_defs/integration/abi.rs deleted file mode 100644 index 75c07c23..00000000 --- a/tests/step_defs/integration/abi.rs +++ /dev/null @@ -1,29 +0,0 @@ -use algonaut::algod::v2::Algod; -use algonaut::algod::AlgodBuilder; -use async_trait::async_trait; -use cucumber::{given, WorldInit}; -use std::convert::Infallible; - -#[derive(Default, Debug, WorldInit)] -pub struct World { - algod: Option, -} - -#[async_trait(?Send)] -impl cucumber::World for World { - type Error = Infallible; - - async fn new() -> Result { - Ok(Self::default()) - } -} - -#[given(expr = "an algod v2 client")] -async fn an_algod_v2_client(w: &mut World) { - let algod = AlgodBuilder::new() - .bind("http://localhost:60000") - .auth("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") - .build_v2() - .unwrap(); - w.algod = Some(algod) -} diff --git a/tests/step_defs/integration/algod.rs b/tests/step_defs/integration/algod.rs deleted file mode 100644 index 8b77ff66..00000000 --- a/tests/step_defs/integration/algod.rs +++ /dev/null @@ -1,62 +0,0 @@ -use algonaut::algod::v2::Algod; -use algonaut::algod::AlgodBuilder; -use algonaut_core::Round; -use algonaut_model::algod::v2::NodeStatus; -use async_trait::async_trait; -use cucumber::{given, then, when, WorldInit}; -use std::convert::Infallible; - -#[derive(Default, Debug, WorldInit)] -pub struct World { - algod_client: Option, - node_status: Option, - last_round: Option, -} - -#[async_trait(?Send)] -impl cucumber::World for World { - type Error = Infallible; - - async fn new() -> Result { - Ok(Self::default()) - } -} - -#[given(expr = "an algod client")] -async fn an_algod_client(w: &mut World) { - let algod = AlgodBuilder::new() - .bind("http://localhost:60000") - .auth("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") - .build_v2() - .unwrap(); - - w.algod_client = Some(algod) -} - -#[then(expr = "the node should be healthy")] -async fn node_is_healthy(w: &mut World) { - let algod_client = w.algod_client.as_ref().unwrap(); - algod_client.health().await.unwrap(); -} - -#[when(expr = "I get the status")] -async fn i_get_the_status(w: &mut World) { - let algod_client = w.algod_client.as_ref().unwrap(); - let status = algod_client.status().await.unwrap(); - w.last_round = Some(status.last_round); - w.node_status = Some(status); -} - -#[when(expr = "I get status after this block")] -async fn i_get_the_status_after_this_block(w: &mut World) { - let algod_client = w.algod_client.as_ref().unwrap(); - let block = w.last_round.unwrap(); - algod_client.status_after_round(Round(block)).await.unwrap(); -} - -#[then(expr = "I can get the block info")] -async fn i_can_get_the_block_info(w: &mut World) { - let algod_client = w.algod_client.as_ref().unwrap(); - let last_round = w.last_round.unwrap(); - algod_client.block(Round(last_round)).await.unwrap(); -} diff --git a/tests/step_defs/integration/applications.rs b/tests/step_defs/integration/applications.rs index 36bf75fd..92fc7ee6 100644 --- a/tests/step_defs/integration/applications.rs +++ b/tests/step_defs/integration/applications.rs @@ -49,7 +49,8 @@ impl cucumber::World for World { #[given(expr = "an algod client")] async fn an_algod_client(_: &mut World) { - // do nothing - we don't support v1 + // Do nothing - we don't support v1 + // The reference (Go) SDK doesn't use it in the definitions } #[given(expr = "a kmd client")] diff --git a/tests/step_defs/integration/mod.rs b/tests/step_defs/integration/mod.rs index 4dd12ffb..c0e90580 100644 --- a/tests/step_defs/integration/mod.rs +++ b/tests/step_defs/integration/mod.rs @@ -1,3 +1 @@ -pub mod abi; -pub mod algod; pub mod applications;