From a0c494c7301b4ef5994df293b42ba58d3736b802 Mon Sep 17 00:00:00 2001
From: librelois <c@elo.tf>
Date: Sat, 5 Dec 2020 02:28:41 +0100
Subject: [PATCH] ref+tests: make sub-commands testable and test it

---
 Cargo.lock      | 141 +++++++++++++++++++++++++++++++++++++++++++++
 Cargo.toml      |   3 +
 src/client.rs   |  37 ++++++++++--
 src/commands.rs | 148 ++++++++++++++++++++++++++++++++++++++++++++++++
 src/main.rs     |  69 +++++-----------------
 5 files changed, 336 insertions(+), 62 deletions(-)
 create mode 100644 src/commands.rs

diff --git a/Cargo.lock b/Cargo.lock
index bbfff64..4c30a1c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -15,6 +15,15 @@ version = "0.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e"
 
+[[package]]
+name = "aho-corasick"
+version = "0.7.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5"
+dependencies = [
+ "memchr",
+]
+
 [[package]]
 name = "ansi_term"
 version = "0.11.0"
@@ -169,12 +178,24 @@ version = "0.8.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b"
 
+[[package]]
+name = "difference"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198"
+
 [[package]]
 name = "doc-comment"
 version = "0.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
 
+[[package]]
+name = "downcast"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bb454f0228b18c7f4c3b0ebbee346ed9c52e7443b0999cd543ff3571205701d"
+
 [[package]]
 name = "either"
 version = "1.6.1"
@@ -212,6 +233,15 @@ dependencies = [
  "synstructure",
 ]
 
+[[package]]
+name = "float-cmp"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1267f4ac4f343772758f7b1bdcbe767c218bbab93bb432acbf5162bbf85a6c4"
+dependencies = [
+ "num-traits",
+]
+
 [[package]]
 name = "fnv"
 version = "1.0.7"
@@ -243,6 +273,12 @@ dependencies = [
  "percent-encoding",
 ]
 
+[[package]]
+name = "fragile"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69a039c3498dc930fe810151a34ba0c1c70b02b8625035592e74432f678591f2"
+
 [[package]]
 name = "fuchsia-zircon"
 version = "0.3.3"
@@ -394,6 +430,7 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "graphql_client",
+ "mockall",
  "reqwest",
  "serde",
  "structopt",
@@ -664,6 +701,33 @@ dependencies = [
  "ws2_32-sys",
 ]
 
+[[package]]
+name = "mockall"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41cabea45a7fc0e37093f4f30a5e2b62602253f91791c057d5f0470c63260c3d"
+dependencies = [
+ "cfg-if 0.1.10",
+ "downcast",
+ "fragile",
+ "lazy_static",
+ "mockall_derive",
+ "predicates",
+ "predicates-tree",
+]
+
+[[package]]
+name = "mockall_derive"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c461918bf7f59eefb1459252756bf2351a995d6bd510d0b2061bd86bcdabfa6"
+dependencies = [
+ "cfg-if 0.1.10",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "native-tls"
 version = "0.2.6"
@@ -693,6 +757,21 @@ dependencies = [
  "winapi 0.3.9",
 ]
 
+[[package]]
+name = "normalize-line-endings"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
+
+[[package]]
+name = "num-traits"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
+dependencies = [
+ "autocfg",
+]
+
 [[package]]
 name = "num_cpus"
 version = "1.13.0"
@@ -824,6 +903,35 @@ version = "0.2.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857"
 
+[[package]]
+name = "predicates"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96bfead12e90dccead362d62bb2c90a5f6fc4584963645bc7f71a735e0b0735a"
+dependencies = [
+ "difference",
+ "float-cmp",
+ "normalize-line-endings",
+ "predicates-core",
+ "regex",
+]
+
+[[package]]
+name = "predicates-core"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06075c3a3e92559ff8929e7a280684489ea27fe44805174c3ebd9328dcb37178"
+
+[[package]]
+name = "predicates-tree"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e63c4859013b38a76eca2414c64911fba30def9e3202ac461a2d22831220124"
+dependencies = [
+ "predicates-core",
+ "treeline",
+]
+
 [[package]]
 name = "proc-macro-error"
 version = "1.0.4"
@@ -913,6 +1021,24 @@ version = "0.1.57"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
 
+[[package]]
+name = "regex"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38cf2c13ed4745de91a5eb834e11c00bcc3709e773173b2ce4c56c9fbde04b9c"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+ "thread_local",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b181ba2dcf07aaccad5448e8ead58db5b742cf85dfe035e2227f137a539a189"
+
 [[package]]
 name = "remove_dir_all"
 version = "0.5.3"
@@ -1147,6 +1273,15 @@ dependencies = [
  "unicode-width",
 ]
 
+[[package]]
+name = "thread_local"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14"
+dependencies = [
+ "lazy_static",
+]
+
 [[package]]
 name = "tinyvec"
 version = "1.1.0"
@@ -1241,6 +1376,12 @@ dependencies = [
  "tracing",
 ]
 
+[[package]]
+name = "treeline"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7f741b240f1a48843f9b8e0444fb55fb2a4ff67293b50a9179dfd5ea67f8d41"
+
 [[package]]
 name = "try-lock"
 version = "0.2.3"
diff --git a/Cargo.toml b/Cargo.toml
index fb731be..8b7635f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -13,3 +13,6 @@ graphql_client = "0.9.0"
 reqwest = { version = "0.10.9", features = ["blocking", "json"] }
 serde = { version = "1.0.105", features = ["derive"] }
 structopt = "0.3.18"
+
+[dev-dependencies]
+mockall = "0.8.0"
diff --git a/src/client.rs b/src/client.rs
index 8aede5c..c6926f7 100644
--- a/src/client.rs
+++ b/src/client.rs
@@ -1,17 +1,42 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// This program 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, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program 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 this program.  If not, see <https://www.gnu.org/licenses/>.
+
 use crate::*;
 
-pub(crate) struct Client(reqwest::blocking::Client);
+#[cfg_attr(test, allow(dead_code))]
+pub(crate) struct Client {
+    inner: reqwest::blocking::Client,
+    server_url: String,
+}
 
+#[cfg_attr(test, mockall::automock, allow(dead_code))]
 impl Client {
-    pub(crate) fn new() -> Self {
-        Client(reqwest::blocking::Client::new())
+    pub(crate) fn new(server_url: String) -> Self {
+        Client {
+            inner: reqwest::blocking::Client::new(),
+            server_url,
+        }
     }
-    pub(crate) fn send_gql_query<Req: serde::Serialize, ResData: serde::de::DeserializeOwned>(
+    pub(crate) fn send_gql_query<
+        Req: 'static + serde::Serialize,
+        ResData: 'static + serde::de::DeserializeOwned,
+    >(
         &self,
         request_body: &Req,
-        server_url: &str,
     ) -> anyhow::Result<ResData> {
-        let request = self.0.post(server_url).json(request_body);
+        let request = self.inner.post(&self.server_url).json(request_body);
 
         let start_time = Instant::now();
         let response = request.send()?;
diff --git a/src/commands.rs b/src/commands.rs
new file mode 100644
index 0000000..d03ff59
--- /dev/null
+++ b/src/commands.rs
@@ -0,0 +1,148 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// This program 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, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program 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 this program.  If not, see <https://www.gnu.org/licenses/>.
+
+use crate::*;
+
+#[derive(StructOpt)]
+pub(crate) enum Command {
+    /// Get account balance
+    Balance {
+        pubkey_or_script: String,
+        #[structopt(short, long)]
+        ud_unit: bool,
+    },
+    /// Get current UD value
+    CurrentUd,
+}
+
+pub(crate) fn balance<W: Write>(
+    client: &Client,
+    out: &mut W,
+    pubkey_or_script: &str,
+    ud_unit: bool,
+) -> anyhow::Result<()> {
+    let request_body = BalanceQuery::build_query(balance_query::Variables {
+        script: pubkey_or_script.to_owned(),
+        with_ud: ud_unit,
+    });
+
+    let balance_query::ResponseData {
+        balance: balance_query::BalanceQueryBalance { amount },
+        current_ud: current_ud_opt,
+    } = client.send_gql_query(&request_body)?;
+
+    if let Some(balance_query::BalanceQueryCurrentUd { amount: ud_amount }) = current_ud_opt {
+        writeln!(
+            out,
+            "The balance of account '{}' is {:.2} UDĞ1 !",
+            pubkey_or_script,
+            amount as f64 / ud_amount as f64,
+        )?;
+    } else {
+        writeln!(
+            out,
+            "The balance of account '{}' is {}.{:02} Ğ1 !",
+            pubkey_or_script,
+            amount / 100,
+            amount % 100
+        )?;
+    }
+    Ok(())
+}
+
+pub(crate) fn current_ud<W: Write>(client: &Client, out: &mut W) -> anyhow::Result<()> {
+    let request_body = CurrentUdQuery::build_query(current_ud_query::Variables);
+
+    if let current_ud_query::ResponseData {
+        current_ud: Some(current_ud_query::CurrentUdQueryCurrentUd { amount }),
+    } = client.send_gql_query(&request_body)?
+    {
+        let int_part = amount / 100;
+        let dec_part = amount % 100;
+        writeln!(
+            out,
+            "The current UD value is {}.{:02} Ğ1 !",
+            int_part, dec_part
+        )?;
+    } else {
+        writeln!(out, "server with empty blockchain")?;
+    }
+
+    Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_balance() -> anyhow::Result<()> {
+        let mut client = Client::default();
+        client
+            .expect_send_gql_query::<graphql_client::QueryBody<balance_query::Variables>, _>()
+            .returning(|_| {
+                Ok(balance_query::ResponseData {
+                    balance: balance_query::BalanceQueryBalance { amount: 2_046 },
+                    current_ud: None,
+                })
+            });
+        let mut out = Vec::new();
+        balance(&client, &mut out, "toto", false)?;
+        let output = std::str::from_utf8(&out)?;
+
+        assert_eq!(output, "The balance of account 'toto' is 20.46 Ğ1 !\n");
+
+        Ok(())
+    }
+
+    #[test]
+    fn test_balance_with_ud_unit() -> anyhow::Result<()> {
+        let mut client = Client::default();
+        client
+            .expect_send_gql_query::<graphql_client::QueryBody<balance_query::Variables>, _>()
+            .returning(|_| {
+                Ok(balance_query::ResponseData {
+                    balance: balance_query::BalanceQueryBalance { amount: 2_046 },
+                    current_ud: Some(balance_query::BalanceQueryCurrentUd { amount: 1_023 }),
+                })
+            });
+        let mut out = Vec::new();
+        balance(&client, &mut out, "toto", true)?;
+        let output = std::str::from_utf8(&out)?;
+
+        assert_eq!(output, "The balance of account 'toto' is 2.00 UDĞ1 !\n");
+
+        Ok(())
+    }
+
+    #[test]
+    fn test_current_ud() -> anyhow::Result<()> {
+        let mut client = Client::default();
+        client
+            .expect_send_gql_query::<graphql_client::QueryBody<current_ud_query::Variables>, _>()
+            .returning(|_| {
+                Ok(current_ud_query::ResponseData {
+                    current_ud: Some(current_ud_query::CurrentUdQueryCurrentUd { amount: 1_023 }),
+                })
+            });
+        let mut out = Vec::new();
+        current_ud(&client, &mut out)?;
+        let output = std::str::from_utf8(&out)?;
+
+        assert_eq!(output, "The current UD value is 10.23 Ğ1 !\n");
+
+        Ok(())
+    }
+}
diff --git a/src/main.rs b/src/main.rs
index 49380cc..b664c1d 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -25,10 +25,17 @@
 )]
 
 mod client;
+mod commands;
 
+#[cfg(not(test))]
 use crate::client::Client;
+#[cfg(test)]
+use crate::client::MockClient as Client;
+use crate::commands::Command;
+use commands::{balance, current_ud};
 use graphql_client::GraphQLQuery;
 use graphql_client::Response;
+use std::io::Write;
 use std::time::Instant;
 use structopt::StructOpt;
 
@@ -49,71 +56,21 @@ struct CliArgs {
     #[structopt(short, long, default_value = DEFAULT_GVA_SERVER)]
     server: String,
     #[structopt(subcommand)]
-    command: SubCommand,
-}
-
-#[derive(StructOpt)]
-enum SubCommand {
-    /// Get account balance
-    Balance {
-        pubkey_or_script: String,
-        #[structopt(short, long)]
-        ud_unit: bool,
-    },
-    /// Get current UD value
-    CurrentUd,
+    command: Command,
 }
 
 fn main() -> anyhow::Result<()> {
     let cli_args = CliArgs::from_args();
 
-    let client = Client::new();
+    let client = Client::new(cli_args.server);
+    let mut out = std::io::stdout();
 
     match cli_args.command {
-        SubCommand::Balance {
+        Command::Balance {
             pubkey_or_script,
             ud_unit,
-        } => {
-            let request_body = BalanceQuery::build_query(balance_query::Variables {
-                script: pubkey_or_script.clone(),
-                with_ud: ud_unit,
-            });
-
-            let balance_query::ResponseData {
-                balance: balance_query::BalanceQueryBalance { amount },
-                current_ud: current_ud_opt,
-            } = client.send_gql_query(&request_body, &cli_args.server)?;
-
-            if let Some(balance_query::BalanceQueryCurrentUd { amount: ud_amount }) = current_ud_opt
-            {
-                println!(
-                    "The balance of account '{}' is {:.2} UDĞ1 !",
-                    pubkey_or_script,
-                    amount as f64 / ud_amount as f64,
-                );
-            } else {
-                println!(
-                    "The balance of account '{}' is {}.{} Ğ1 !",
-                    pubkey_or_script,
-                    amount / 100,
-                    amount % 100
-                );
-            }
-        }
-        SubCommand::CurrentUd => {
-            let request_body = CurrentUdQuery::build_query(current_ud_query::Variables);
-
-            if let current_ud_query::ResponseData {
-                current_ud: Some(current_ud_query::CurrentUdQueryCurrentUd { amount }),
-            } = client.send_gql_query(&request_body, &cli_args.server)?
-            {
-                let int_part = amount / 100;
-                let dec_part = amount % 100;
-                println!("The current UD value is {}.{} Ğ1 !", int_part, dec_part);
-            } else {
-                println!("server with empty blockchain");
-            }
-        }
+        } => balance(&client, &mut out, &pubkey_or_script, ud_unit)?,
+        Command::CurrentUd => current_ud(&client, &mut out)?,
     }
     Ok(())
 }
-- 
GitLab