Skip to content

Commit

Permalink
Add unit tests to lib that mock out http requests
Browse files Browse the repository at this point in the history
  • Loading branch information
elizabethengelman committed Apr 30, 2024
1 parent 8a097af commit 7829271
Show file tree
Hide file tree
Showing 3 changed files with 257 additions and 22 deletions.
2 changes: 2 additions & 0 deletions cmd/crates/stellar-ledger/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ log = "0.4.21"
once_cell = "1.19.0"
pretty_assertions = "1.2.1"
serial_test = "3.0.0"
httpmock = "0.7.0-rc.1"


[features]
emulator-tests = []
13 changes: 9 additions & 4 deletions cmd/crates/stellar-ledger/src/emulator_tests.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
use soroban_env_host::xdr::Transaction;
use std::vec;

use crate::signer::Stellar;
use ledger_transport::Exchange;
use serde::Deserialize;
use soroban_env_host::xdr::Transaction;
use soroban_env_host::xdr::{self, Operation, OperationBody, Uint256};
use std::vec;

use crate::speculos::Speculos;
use crate::{get_zemu_transport, LedgerError, LedgerOptions, LedgerSigner};
use crate::transport_zemu_http::TransportZemuHttp;
use crate::{LedgerError, LedgerOptions, LedgerSigner};

use std::sync::Arc;
use std::{collections::HashMap, str::FromStr, time::Duration};
Expand Down Expand Up @@ -342,6 +343,10 @@ struct EventsResponse {
events: Vec<EmulatorEvent>,
}

fn get_zemu_transport(host: &str, port: u16) -> Result<impl Exchange, LedgerError> {
Ok(TransportZemuHttp::new(host, port))
}

async fn wait_for_emulator_start_text(ui_host_port: u16) {
sleep(Duration::from_secs(1)).await;

Expand Down
264 changes: 246 additions & 18 deletions cmd/crates/stellar-ledger/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
use futures::executor::block_on;
use ledger_transport::{APDUCommand, Exchange};
use ledger_transport_hid::{
hidapi::{HidApi, HidError},
LedgerHIDError, TransportNativeHID,
};
use ledger_transport_hid::{hidapi::HidError, LedgerHIDError};
use sha2::{Digest, Sha256};

use soroban_env_host::xdr::{Hash, Transaction};
Expand All @@ -15,7 +12,6 @@ use stellar_xdr::curr::{
};

use crate::signer::{Error, Stellar};
use crate::transport_zemu_http::TransportZemuHttp;

mod signer;
mod speculos;
Expand Down Expand Up @@ -334,18 +330,250 @@ fn hd_path_to_bytes(hd_path: &slip10::BIP32Path) -> Vec<u8> {
.collect::<Vec<u8>>()
}

/// Gets a transport connection for a ledger device
/// # Errors
/// Returns an error if there is an issue with connecting with the device
pub fn get_transport() -> Result<impl Exchange, LedgerError> {
// instantiate the connection to Ledger, this will return an error if Ledger is not connected
let hidapi = HidApi::new().map_err(LedgerError::HidApiError)?;
TransportNativeHID::new(&hidapi).map_err(LedgerError::LedgerHidError)
}
#[cfg(test)]
mod test {
use std::str::FromStr;

use httpmock::prelude::*;
use serde_json::json;

use crate::transport_zemu_http::TransportZemuHttp;

use soroban_env_host::xdr::Transaction;
use std::vec;

use crate::signer::Stellar;
use soroban_env_host::xdr::{self, Operation, OperationBody, Uint256};

use crate::{LedgerError, LedgerOptions, LedgerSigner};

use stellar_xdr::curr::{
Memo, MuxedAccount, PaymentOp, Preconditions, SequenceNumber, TransactionExt,
};

const TEST_NETWORK_PASSPHRASE: &str = "Test SDF Network ; September 2015";

#[tokio::test]
async fn test_get_public_key() {
let server = MockServer::start();
let mock_server = server.mock(|when, then| {
when.method(POST)
.path("/")
.header("accept", "application/json")
.header("content-type", "application/json")
.json_body(json!({ "apduHex": "e00200000d038000002c8000009480000000" }));
then.status(200)
.header("content-type", "application/json")
.json_body(json!({"data": "e93388bbfd2fbd11806dd0bd59cea9079e7cc70ce7b1e154f114cdfe4e466ecd9000"}));
});

let transport = TransportZemuHttp::new(&server.host(), server.port());
let ledger_options = Some(LedgerOptions {
exchange: transport,
hd_path: slip10::BIP32Path::from_str("m/44'/148'/0'").unwrap(),
});

let ledger = LedgerSigner::new(TEST_NETWORK_PASSPHRASE, ledger_options);
match ledger.get_public_key(0).await {
Ok(public_key) => {
let public_key_string = public_key.to_string();
// This is determined by the seed phrase used to start up the emulator
let expected_public_key =
"GDUTHCF37UX32EMANXIL2WOOVEDZ47GHBTT3DYKU6EKM37SOIZXM2FN7";
assert_eq!(public_key_string, expected_public_key);
}
Err(e) => {
println!("{e}");
assert!(false);
}
}

mock_server.assert();
}

#[tokio::test]
async fn test_get_app_configuration() {
let server = MockServer::start();
let mock_server = server.mock(|when, then| {
when.method(POST)
.path("/")
.header("accept", "application/json")
.header("content-type", "application/json")
.json_body(json!({ "apduHex": "e006000000" }));
then.status(200)
.header("content-type", "application/json")
.json_body(json!({"data": "000500039000"}));
});

let transport = TransportZemuHttp::new(&server.host(), server.port());
let ledger_options = Some(LedgerOptions {
exchange: transport,
hd_path: slip10::BIP32Path::from_str("m/44'/148'/0'").unwrap(),
});

let ledger = LedgerSigner::new(TEST_NETWORK_PASSPHRASE, ledger_options);
match ledger.get_app_configuration().await {
Ok(config) => {
assert_eq!(config, vec![0, 5, 0, 3]);
}
Err(e) => {
println!("{e}");
assert!(false);
}
};

mock_server.assert();
}

#[tokio::test]
async fn test_sign_tx() {
let server = MockServer::start();
let mock_request_1 = server.mock(|when, then| {
when.method(POST)
.path("/")
.header("accept", "application/json")
.header("content-type", "application/json")
.json_body(json!({ "apduHex": "e004008089038000002c8000009480000000cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472000000020000000000000000000000000000000000000000000000000000000000000000000000000000006400000000000000010000000000000001000000075374656c6c6172000000000100000001000000000000000000000000" }));
then.status(200)
.header("content-type", "application/json")
.json_body(json!({"data": "9000"}));
});

let mock_request_2 = server.mock(|when, then| {
when.method(POST)
.path("/")
.header("accept", "application/json")
.header("content-type", "application/json")
.json_body(json!({ "apduHex": "e0048000500000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006400000000" }));
then.status(200)
.header("content-type", "application/json")
.json_body(json!({"data": "5c2f8eb41e11ab922800071990a25cf9713cc6e7c43e50e0780ddc4c0c6da50c784609ef14c528a12f520d8ea9343b49083f59c51e3f28af8c62b3edeaade60e9000"}));
});

let transport = TransportZemuHttp::new(&server.host(), server.port());
let ledger_options = Some(LedgerOptions {
exchange: transport,
hd_path: slip10::BIP32Path::from_str("m/44'/148'/0'").unwrap(),
});
let ledger = LedgerSigner::new(TEST_NETWORK_PASSPHRASE, ledger_options);
let path = slip10::BIP32Path::from_str("m/44'/148'/0'").unwrap();

let fake_source_acct = [0 as u8; 32];
let fake_dest_acct = [0 as u8; 32];
let tx = Transaction {
source_account: MuxedAccount::Ed25519(Uint256(fake_source_acct)),
fee: 100,
seq_num: SequenceNumber(1),
cond: Preconditions::None,
memo: Memo::Text("Stellar".as_bytes().try_into().unwrap()),
ext: TransactionExt::V0,
operations: [Operation {
source_account: Some(MuxedAccount::Ed25519(Uint256(fake_source_acct))),
body: OperationBody::Payment(PaymentOp {
destination: MuxedAccount::Ed25519(Uint256(fake_dest_acct)),
asset: xdr::Asset::Native,
amount: 100,
}),
}]
.try_into()
.unwrap(),
};

let result = ledger.sign_transaction(path, tx).await;
match result {
Ok(response) => {
assert_eq!( hex::encode(response), "5c2f8eb41e11ab922800071990a25cf9713cc6e7c43e50e0780ddc4c0c6da50c784609ef14c528a12f520d8ea9343b49083f59c51e3f28af8c62b3edeaade60e");
}
Err(e) => {
println!("{e}");
assert!(false);
}
};
mock_request_1.assert();
mock_request_2.assert();
}

#[tokio::test]
async fn test_sign_tx_hash_when_hash_signing_is_not_enabled() {
//when hash signing isn't enabled on the device we expect an error
let server = MockServer::start();
let mock_server = server.mock(|when, then| {
when.method(POST)
.path("/")
.header("accept", "application/json")
.header("content-type", "application/json")
.json_body(json!({ "apduHex": "e00800004d038000002c800000948000000033333839653966306631613635663139373336636163663534346332653832353331336538343437663536393233336262386462333961613630376338383839" }));
then.status(200)
.header("content-type", "application/json")
.json_body(json!({"data": "6c66"}));
});

let transport = TransportZemuHttp::new(&server.host(), server.port());
let ledger_options = Some(LedgerOptions {
exchange: transport,
hd_path: slip10::BIP32Path::from_str("m/44'/148'/0'").unwrap(),
});
let ledger = LedgerSigner::new(TEST_NETWORK_PASSPHRASE, ledger_options);

let path = slip10::BIP32Path::from_str("m/44'/148'/0'").unwrap();
let test_hash =
"3389e9f0f1a65f19736cacf544c2e825313e8447f569233bb8db39aa607c8889".as_bytes();

let result = ledger.sign_transaction_hash(path, test_hash.into()).await;
if let Err(LedgerError::APDUExchangeError(msg)) = result {
assert_eq!(msg, "Ledger APDU retcode: 0x6C66");
// this error code is SW_TX_HASH_SIGNING_MODE_NOT_ENABLED https://github.com/LedgerHQ/app-stellar/blob/develop/docs/COMMANDS.md
} else {
panic!("Unexpected result: {:?}", result);
}

mock_server.assert();
}

#[tokio::test]
async fn test_sign_tx_hash_when_hash_signing_is_enabled() {
let server = MockServer::start();
let mock_server = server.mock(|when, then| {
when.method(POST)
.path("/")
.header("accept", "application/json")
.header("content-type", "application/json")
.json_body(json!({ "apduHex": "e00800002d038000002c80000094800000003389e9f0f1a65f19736cacf544c2e825313e8447f569233bb8db39aa607c8889" }));
then.status(200)
.header("content-type", "application/json")
.json_body(json!({"data": "6970b9c9d3a6f4de7fb93e8d3920ec704fc4fece411873c40570015bbb1a60a197622bc3bf5644bb38ae73e1b96e4d487d716d142d46c7e944f008dece92df079000"}));
});

let transport = TransportZemuHttp::new(&server.host(), server.port());
let ledger_options = Some(LedgerOptions {
exchange: transport,
hd_path: slip10::BIP32Path::from_str("m/44'/148'/0'").unwrap(),
});
let ledger = LedgerSigner::new(TEST_NETWORK_PASSPHRASE, ledger_options);
let path = slip10::BIP32Path::from_str("m/44'/148'/0'").unwrap();
let mut test_hash = vec![0u8; 32];

match hex::decode_to_slice(
"3389e9f0f1a65f19736cacf544c2e825313e8447f569233bb8db39aa607c8889",
&mut test_hash as &mut [u8],
) {
Ok(()) => {}
Err(e) => {
panic!("Unexpected result: {e}");
}
}

let result = ledger.sign_transaction_hash(path, test_hash).await;

/// Gets a transport connection for a the Zemu emulator
/// # Errors
/// Returns an error if there is an issue with connecting with the device
pub fn get_zemu_transport(host: &str, port: u16) -> Result<impl Exchange, LedgerError> {
Ok(TransportZemuHttp::new(host, port))
match result {
Ok(response) => {
assert_eq!( hex::encode(response), "6970b9c9d3a6f4de7fb93e8d3920ec704fc4fece411873c40570015bbb1a60a197622bc3bf5644bb38ae73e1b96e4d487d716d142d46c7e944f008dece92df07");
}
Err(e) => {
panic!("Unexpected result: {e}");
}
}

mock_server.assert();
}
}

0 comments on commit 7829271

Please sign in to comment.