Skip to content
Snippets Groups Projects
Commit da4b25bf authored by Hugo Trentesaux's avatar Hugo Trentesaux
Browse files

feat(cucumber) add certification

parent db18f612
No related branches found
No related tags found
No related merge requests found
...@@ -1517,6 +1517,7 @@ dependencies = [ ...@@ -1517,6 +1517,7 @@ dependencies = [
name = "duniter-end2end-tests" name = "duniter-end2end-tests"
version = "3.0.0" version = "3.0.0"
dependencies = [ dependencies = [
"anyhow",
"async-trait", "async-trait",
"clap", "clap",
"ctrlc", "ctrlc",
......
...@@ -9,6 +9,7 @@ repository = 'https://git.duniter.org/nodes/rust/duniter-v2s' ...@@ -9,6 +9,7 @@ repository = 'https://git.duniter.org/nodes/rust/duniter-v2s'
version = '3.0.0' version = '3.0.0'
[dev-dependencies] [dev-dependencies]
anyhow = "1.0"
async-trait = "0.1" async-trait = "0.1"
clap = { version = "3.0", features = ["derive"] } clap = { version = "3.0", features = ["derive"] }
ctrlc = "3.2.2" ctrlc = "3.2.2"
......
# Duniter-v2s end2end tests # Duniter-v2s end2end tests
## cucumber functionnal tests ## Cucumber functionnal tests
We use [cucumber] to be able to describe test scenarios in human language. We use [cucumber] to be able to describe test scenarios in human language.
...@@ -20,7 +20,7 @@ Feature: Balance transfer ...@@ -20,7 +20,7 @@ Feature: Balance transfer
Then dave should have 5 ĞD 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 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`. `/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 ...@@ -55,8 +55,8 @@ Each scenario is a list of steps. In our context (blockchain), only the `When` a
List of possible actions: List of possible actions:
- transfer: `alice send 5 ĞD to bob` - transfer: `alice sends 5 ĞD to bob`
- transfer_ud: `alice send 3 UD to bob` - transfer_ud: `alice sends 3 UD to bob`
- transfer_all: `alice sends all her ĞDs to bob` - transfer_all: `alice sends all her ĞDs to bob`
#### Then #### Then
...@@ -96,7 +96,7 @@ Amounts must be expressed as an integer of `ĞD` or `UD`, decimal numbers are no ...@@ -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 If you need more precision, you can express amounts in cents of ĞD (write `cĞD`), or in thousandths
of UD (write `mUD`). of UD (write `mUD`).
### genesis state ### Genesis state
Each scenario bootstraps its own blockchain with its own 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 ...@@ -117,17 +117,22 @@ For some scenarios, you may need to perform an action (When) that fails voluntar
### Run cucumber functional tests ### 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 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` 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* 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 ### Contribute to the code that runs the tests
......
@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
{
"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"
}
// 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(())
}
...@@ -17,10 +17,12 @@ ...@@ -17,10 +17,12 @@
#![allow(clippy::enum_variant_names, dead_code, unused_imports)] #![allow(clippy::enum_variant_names, dead_code, unused_imports)]
pub mod balances; pub mod balances;
pub mod cert;
#[subxt::subxt(runtime_metadata_path = "../resources/metadata.scale")] #[subxt::subxt(runtime_metadata_path = "../resources/metadata.scale")]
pub mod node_runtime {} pub mod node_runtime {}
use anyhow::anyhow;
use serde_json::Value; use serde_json::Value;
use sp_keyring::AccountKeyring; use sp_keyring::AccountKeyring;
use std::io::prelude::*; use std::io::prelude::*;
...@@ -55,7 +57,6 @@ struct FullNode { ...@@ -55,7 +57,6 @@ struct FullNode {
} }
pub async fn spawn_node(maybe_genesis_conf_file: Option<PathBuf>) -> (Api, Client, Process) { 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(|_| { let duniter_binary_path = std::env::var("DUNITER_BINARY_PATH").unwrap_or_else(|_| {
if std::path::Path::new(DUNITER_DOCKER_PATH).exists() { if std::path::Path::new(DUNITER_DOCKER_PATH).exists() {
DUNITER_DOCKER_PATH.to_owned() DUNITER_DOCKER_PATH.to_owned()
......
...@@ -28,6 +28,8 @@ use std::sync::{ ...@@ -28,6 +28,8 @@ use std::sync::{
Arc, Arc,
}; };
// ===== world =====
#[derive(WorldInit)] #[derive(WorldInit)]
pub struct DuniterWorld { pub struct DuniterWorld {
ignore_errors: bool, ignore_errors: bool,
...@@ -119,7 +121,9 @@ fn parse_amount(amount: u64, unit: &str) -> (u64, 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<()> { async fn who_have(world: &mut DuniterWorld, who: String, amount: u64, unit: String) -> Result<()> {
// Parse inputs // Parse inputs
let who = AccountKeyring::from_str(&who).expect("unknown to"); 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 ...@@ -141,6 +145,8 @@ async fn who_have(world: &mut DuniterWorld, who: String, amount: u64, unit: Stri
Ok(()) Ok(())
} }
// ===== when =====
#[when(regex = r"(\d+) blocks? later")] #[when(regex = r"(\d+) blocks? later")]
async fn n_blocks_later(world: &mut DuniterWorld, n: usize) -> Result<()> { async fn n_blocks_later(world: &mut DuniterWorld, n: usize) -> Result<()> {
for _ in 0..n { for _ in 0..n {
...@@ -149,7 +155,7 @@ async fn n_blocks_later(world: &mut DuniterWorld, n: usize) -> Result<()> { ...@@ -149,7 +155,7 @@ async fn n_blocks_later(world: &mut DuniterWorld, n: usize) -> Result<()> {
Ok(()) 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( async fn transfer(
world: &mut DuniterWorld, world: &mut DuniterWorld,
from: String, from: String,
...@@ -175,7 +181,7 @@ async fn transfer( ...@@ -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<()> { async fn send_all_to(world: &mut DuniterWorld, from: String, to: String) -> Result<()> {
// Parse inputs // Parse inputs
let from = AccountKeyring::from_str(&from).expect("unknown from"); 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 ...@@ -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 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)")] #[then(regex = r"([a-zA-Z]+) should have (\d+) (ĞD|cĞD)")]
async fn should_have( async fn should_have(
world: &mut DuniterWorld, world: &mut DuniterWorld,
...@@ -232,6 +249,52 @@ async fn monetary_mass_should_be(world: &mut DuniterWorld, amount: u64, cents: u ...@@ -232,6 +249,52 @@ async fn monetary_mass_should_be(world: &mut DuniterWorld, amount: u64, cents: u
Ok(()) 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)] #[derive(clap::Args)]
struct CustomOpts { struct CustomOpts {
/// Keep running /// Keep running
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment