diff --git a/Cargo.lock b/Cargo.lock index f7fbb6a6aa5792381f4df4cc842aeac7eb13d9b1..3908137f8b6ab490466087abd8794cb97a75f058 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1517,6 +1517,7 @@ dependencies = [ name = "duniter-end2end-tests" version = "3.0.0" dependencies = [ + "anyhow", "async-trait", "clap", "ctrlc", diff --git a/end2end-tests/Cargo.toml b/end2end-tests/Cargo.toml index 8d7b445eeded82e13bb22a059caefa39ad27fd7d..09aae7a4982bc07e83eeeddca11c4e293a1d40e1 100644 --- a/end2end-tests/Cargo.toml +++ b/end2end-tests/Cargo.toml @@ -9,6 +9,7 @@ repository = 'https://git.duniter.org/nodes/rust/duniter-v2s' version = '3.0.0' [dev-dependencies] +anyhow = "1.0" async-trait = "0.1" clap = { version = "3.0", features = ["derive"] } ctrlc = "3.2.2" diff --git a/end2end-tests/README.md b/end2end-tests/README.md index c15fc019df0c420eb01b9306143f20885a764947..6bea5535161d2e486fc8cae70415ae783728139b 100644 --- a/end2end-tests/README.md +++ b/end2end-tests/README.md @@ -1,6 +1,6 @@ # Duniter-v2s end2end tests -## cucumber functionnal tests +## Cucumber functionnal tests We use [cucumber] to be able to describe test scenarios in human language. @@ -20,7 +20,7 @@ Feature: Balance transfer Then dave should have 5 ÄžD ``` -### create a new functional test +### Create a new functional test To create a new test scenario, simply create a new file with a name of your choice in the `/cucumber-features` folder and give it the extension `.feature`. @@ -55,8 +55,8 @@ Each scenario is a list of steps. In our context (blockchain), only the `When` a List of possible actions: -- transfer: `alice send 5 ÄžD to bob` -- transfer_ud: `alice send 3 UD to bob` +- transfer: `alice sends 5 ÄžD to bob` +- transfer_ud: `alice sends 3 UD to bob` - transfer_all: `alice sends all her ÄžDs to bob` #### Then @@ -96,7 +96,7 @@ Amounts must be expressed as an integer of `ÄžD` or `UD`, decimal numbers are no If you need more precision, you can express amounts in cents of ÄžD (write `cÄžD`), or in thousandths of UD (write `mUD`). -### genesis state +### Genesis state Each scenario bootstraps its own blockchain with its own genesis state. @@ -117,17 +117,22 @@ For some scenarios, you may need to perform an action (When) that fails voluntar ### Run cucumber functional tests +The cucumber tests use the last debug binary in your `target` folder. Make sure this binary corresponds to the executable you want to test by running `cargo build` before. + To run the cucumber tests, you will need to have the rust toolchain installed locally. To run all the scenarios (there are many) use the command: `cargo cucumber` -You can filter the `.feature` files to run with the option `i`, for instante: +You can filter the `.feature` files to run with the option `i`, for instance: ``` cargo cucumber -i monetary* ``` -Will only run `.feature` files that start with `"monetary"`. +will only run `.feature` files that start with `"monetary"`. + +The features will be tested in parallel and logs files will be written in the `end2end-tests` folder. +If you get an `Error: Timeout`, look at the logs to understand why Duniter did not launch successfully. You can also set the environment variable `DUNITER_END2END_TESTS_SPAWN_NODE_TIMEOUT` to increase the timeout for node spawn. ### Contribute to the code that runs the tests diff --git a/end2end-tests/cucumber-features/certification.feature b/end2end-tests/cucumber-features/certification.feature new file mode 100644 index 0000000000000000000000000000000000000000..aa4e6ce9e89bd84e89c8512a271939a39c03987a --- /dev/null +++ b/end2end-tests/cucumber-features/certification.feature @@ -0,0 +1,7 @@ +@genesis.wot + +Feature: Certification + + Scenario: Dave certifies Alice + When dave certifies alice + Then alice should be certified by dave \ No newline at end of file diff --git a/end2end-tests/cucumber-genesis/wot.json b/end2end-tests/cucumber-genesis/wot.json new file mode 100644 index 0000000000000000000000000000000000000000..988e6857f07b387758e082040a684a2f255029db --- /dev/null +++ b/end2end-tests/cucumber-genesis/wot.json @@ -0,0 +1,66 @@ +{ + "first_ud": 1000, + "first_ud_reeval": 100, + "identities": { + "Alice": { + "balance": 1000, + "certs": ["Bob", "Charlie", "Dave"], + "pubkey": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" + }, + "Bob": { + "balance": 1000, + "certs": ["Alice", "Charlie", "Dave"], + "pubkey": "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + }, + "Charlie": { + "balance": 1000, + "certs": ["Alice", "Bob"], + "pubkey": "5FLSigC9HGRKVhB9FiEo4Y3koPsNmBmLJbpXg2mp1hXcS59Y" + }, + "Dave": { + "balance": 1000, + "certs": [], + "pubkey": "5DAAnrj7VHTznn2AWBemMuyBwZWs6FNFjdyVXUeYum3PTXFy" + } + }, + "parameters": { + "babe_epoch_duration": 30, + "cert_period": 15, + "cert_max_by_issuer": 10, + "cert_min_received_cert_to_issue_cert": 2, + "cert_renewable_period": 50, + "cert_validity_period": 1000, + "idty_confirm_period": 40, + "idty_creation_period": 50, + "membership_period": 1000, + "membership_renewable_period": 50, + "pending_membership_period": 500, + "ud_creation_period": 10, + "ud_reeval_period": 100, + "smith_cert_period": 15, + "smith_cert_max_by_issuer": 8, + "smith_cert_min_received_cert_to_issue_cert": 2, + "smith_cert_renewable_period": 50, + "smith_cert_validity_period": 1000, + "smith_membership_period": 1000, + "smith_membership_renewable_period": 20, + "smith_pending_membership_period": 500, + "smiths_wot_first_cert_issuable_on": 20, + "smiths_wot_min_cert_for_membership": 2, + "wot_first_cert_issuable_on": 20, + "wot_min_cert_for_create_idty_right": 2, + "wot_min_cert_for_membership": 2 + }, + "smiths": { + "Alice": { + "certs": ["Bob", "Charlie"] + }, + "Bob": { + "certs": ["Alice", "Charlie"] + }, + "Charlie": { + "certs": ["Alice", "Bob"] + } + }, + "sudo_key": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" +} diff --git a/end2end-tests/tests/common/cert.rs b/end2end-tests/tests/common/cert.rs new file mode 100644 index 0000000000000000000000000000000000000000..021dfa2ea970f5b75bd89d618739255631be74f6 --- /dev/null +++ b/end2end-tests/tests/common/cert.rs @@ -0,0 +1,43 @@ +// Copyright 2021 Axiom-Team +// +// This file is part of Substrate-Libre-Currency. +// +// Substrate-Libre-Currency is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Substrate-Libre-Currency is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with Substrate-Libre-Currency. If not, see <https://www.gnu.org/licenses/>. + +use super::node_runtime::runtime_types::gdev_runtime; +use super::node_runtime::runtime_types::pallet_certification; +use super::*; +use sp_keyring::AccountKeyring; +use subxt::{sp_runtime::MultiAddress, PairSigner}; + +pub async fn certify( + api: &Api, + client: &Client, + from: AccountKeyring, + to: AccountKeyring, +) -> Result<()> { + let from = PairSigner::new(from.pair()); + let to = to.to_account_id(); + + let _events = create_block_with_extrinsic( + client, + api.tx() + .cert() + .add_cert(to) + .create_signed(&from, ()) + .await?, + ) + .await?; + + Ok(()) +} diff --git a/end2end-tests/tests/common/mod.rs b/end2end-tests/tests/common/mod.rs index 078abd747617b2e2c04a6392e796d5e5ccd75507..d2fa2c758e9a7982215fd8ad39d1185130d15b27 100644 --- a/end2end-tests/tests/common/mod.rs +++ b/end2end-tests/tests/common/mod.rs @@ -17,10 +17,12 @@ #![allow(clippy::enum_variant_names, dead_code, unused_imports)] pub mod balances; +pub mod cert; #[subxt::subxt(runtime_metadata_path = "../resources/metadata.scale")] pub mod node_runtime {} +use anyhow::anyhow; use serde_json::Value; use sp_keyring::AccountKeyring; use std::io::prelude::*; @@ -55,7 +57,6 @@ struct FullNode { } pub async fn spawn_node(maybe_genesis_conf_file: Option<PathBuf>) -> (Api, Client, Process) { - println!("maybe_genesis_conf_file={:?}", maybe_genesis_conf_file); let duniter_binary_path = std::env::var("DUNITER_BINARY_PATH").unwrap_or_else(|_| { if std::path::Path::new(DUNITER_DOCKER_PATH).exists() { DUNITER_DOCKER_PATH.to_owned() diff --git a/end2end-tests/tests/cucumber_tests.rs b/end2end-tests/tests/cucumber_tests.rs index a039bc2b73908db6ac9665f1069a96d7b1ba4a05..cf8661b530142f5813ae54252891aa7c73059aaf 100644 --- a/end2end-tests/tests/cucumber_tests.rs +++ b/end2end-tests/tests/cucumber_tests.rs @@ -28,6 +28,8 @@ use std::sync::{ Arc, }; +// ===== world ===== + #[derive(WorldInit)] pub struct DuniterWorld { ignore_errors: bool, @@ -119,7 +121,9 @@ fn parse_amount(amount: u64, unit: &str) -> (u64, bool) { } } -#[given(regex = r"([a-zA-Z]+) have (\d+) (ÄžD|cÄžD|UD|mUD)")] +// ===== given ===== + +#[given(regex = r"([a-zA-Z]+) ha(?:ve|s) (\d+) (ÄžD|cÄžD|UD|mUD)")] async fn who_have(world: &mut DuniterWorld, who: String, amount: u64, unit: String) -> Result<()> { // Parse inputs let who = AccountKeyring::from_str(&who).expect("unknown to"); @@ -141,6 +145,8 @@ async fn who_have(world: &mut DuniterWorld, who: String, amount: u64, unit: Stri Ok(()) } +// ===== when ===== + #[when(regex = r"(\d+) blocks? later")] async fn n_blocks_later(world: &mut DuniterWorld, n: usize) -> Result<()> { for _ in 0..n { @@ -149,7 +155,7 @@ async fn n_blocks_later(world: &mut DuniterWorld, n: usize) -> Result<()> { Ok(()) } -#[when(regex = r"([a-zA-Z]+) send (\d+) (ÄžD|cÄžD|UD|mUD) to ([a-zA-Z]+)")] +#[when(regex = r"([a-zA-Z]+) sends? (\d+) (ÄžD|cÄžD|UD|mUD) to ([a-zA-Z]+)")] async fn transfer( world: &mut DuniterWorld, from: String, @@ -175,7 +181,7 @@ async fn transfer( } } -#[when(regex = r"([a-zA-Z]+) sends all (?:his|her) (?:ÄžDs?|DUs?) to ([a-zA-Z]+)")] +#[when(regex = r"([a-zA-Z]+) sends? all (?:his|her) (?:ÄžDs?|DUs?) to ([a-zA-Z]+)")] async fn send_all_to(world: &mut DuniterWorld, from: String, to: String) -> Result<()> { // Parse inputs let from = AccountKeyring::from_str(&from).expect("unknown from"); @@ -184,6 +190,17 @@ async fn send_all_to(world: &mut DuniterWorld, from: String, to: String) -> Resu common::balances::transfer_all(world.api(), world.client(), from, to).await } +#[when(regex = r"([a-zA-Z]+) certifies ([a-zA-Z]+)")] +async fn certifies(world: &mut DuniterWorld, from: String, to: String) -> Result<()> { + // Parse inputs + let from = AccountKeyring::from_str(&from).expect("unknown from"); + let to = AccountKeyring::from_str(&to).expect("unknown to"); + + common::cert::certify(world.api(), world.client(), from, to).await +} + +// ===== then ==== + #[then(regex = r"([a-zA-Z]+) should have (\d+) (ÄžD|cÄžD)")] async fn should_have( world: &mut DuniterWorld, @@ -232,6 +249,52 @@ async fn monetary_mass_should_be(world: &mut DuniterWorld, amount: u64, cents: u Ok(()) } +#[then(regex = r"([a-zA-Z]+) should be certified by ([a-zA-Z]+)")] +async fn should_be_certified_by( + world: &mut DuniterWorld, + receiver: String, + issuer: String, +) -> Result<()> { + // Parse inputs + let receiver_account = AccountKeyring::from_str(&receiver) + .expect("unknown to") + .to_account_id(); + let issuer_account = AccountKeyring::from_str(&issuer) + .expect("unknown to") + .to_account_id(); + + let issuer_index = world + .api() + .storage() + .identity() + .identity_index_of(issuer_account, None) + .await? + .unwrap(); + let receiver_index = world + .api() + .storage() + .identity() + .identity_index_of(receiver_account, None) + .await? + .unwrap(); + + let _certification = world + .api() + .storage() + .cert() + .storage_certs_by_issuer(issuer_index, receiver_index, None) + .await? + .ok_or(anyhow::anyhow!( + "no certification found from {} to {}", + issuer, + receiver + )); + + Ok(()) +} + +// ============================================================ + #[derive(clap::Args)] struct CustomOpts { /// Keep running