From 89d0e1fc46c5902dbff34c9d91d303ee588f2ddd Mon Sep 17 00:00:00 2001
From: Hugo Trentesaux <hugo@trentesaux.fr>
Date: Wed, 8 Jun 2022 22:27:02 +0200
Subject: [PATCH] feat(cucumber): add certification

---
 Cargo.lock                                    |  1 +
 end2end-tests/Cargo.toml                      |  1 +
 end2end-tests/README.md                       | 19 ++++--
 .../cucumber-features/certification.feature   |  7 ++
 end2end-tests/cucumber-genesis/wot.json       | 66 +++++++++++++++++++
 end2end-tests/tests/common/cert.rs            | 43 ++++++++++++
 end2end-tests/tests/common/mod.rs             |  3 +-
 end2end-tests/tests/cucumber_tests.rs         | 65 +++++++++++++++++-
 8 files changed, 194 insertions(+), 11 deletions(-)
 create mode 100644 end2end-tests/cucumber-features/certification.feature
 create mode 100644 end2end-tests/cucumber-genesis/wot.json
 create mode 100644 end2end-tests/tests/common/cert.rs

diff --git a/Cargo.lock b/Cargo.lock
index f7fbb6a6a..3908137f8 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 8d7b445ee..09aae7a49 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 c15fc019d..6bea55351 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 000000000..aa4e6ce9e
--- /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 000000000..988e6857f
--- /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 000000000..021dfa2ea
--- /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 078abd747..d2fa2c758 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 a039bc2b7..e117fa477 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,48 @@ 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_else(|| anyhow::anyhow!("no certification found from {} to {}", issuer, receiver));
+
+    Ok(())
+}
+
+// ============================================================
+
 #[derive(clap::Args)]
 struct CustomOpts {
     /// Keep running
-- 
GitLab