Skip to content

Commit

Permalink
Convert block and transaction integration tests to unit tests (#560)
Browse files Browse the repository at this point in the history
  • Loading branch information
raphjaph authored Sep 26, 2022
1 parent 1da4e75 commit 6d1f36f
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 115 deletions.
59 changes: 49 additions & 10 deletions src/subcommand/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -598,7 +598,7 @@ mod tests {
assert_eq!(response.text().unwrap(), expected_response);
}

fn assert_response_regex(&self, path: &str, status: StatusCode, regex: &'static str) {
fn assert_response_regex(&self, path: &str, status: StatusCode, regex: &str) {
let response = self.get(path);
assert_eq!(response.status(), status);
assert_regex_match!(response.text().unwrap(), regex);
Expand Down Expand Up @@ -974,7 +974,7 @@ mod tests {
let test_server = TestServer::new();

test_server.assert_response_regex(
"output/4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0",
"/output/4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0",
StatusCode::OK,
".*<title>Output 4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0</title>.*<h1>Output 4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0</h1>
<h2>Ordinal Ranges</h2>
Expand All @@ -987,7 +987,7 @@ mod tests {
#[test]
fn unknown_output_returns_404() {
TestServer::new().assert_response(
"output/0000000000000000000000000000000000000000000000000000000000000000:0",
"/output/0000000000000000000000000000000000000000000000000000000000000000:0",
StatusCode::NOT_FOUND,
"Output unknown.",
);
Expand All @@ -996,7 +996,7 @@ mod tests {
#[test]
fn invalid_output_returns_400() {
TestServer::new().assert_response(
"output/foo:0",
"/output/foo:0",
StatusCode::BAD_REQUEST,
"Invalid URL: error parsing TXID: odd hex string length 3",
);
Expand Down Expand Up @@ -1038,7 +1038,7 @@ mod tests {
#[test]
fn block_not_found() {
TestServer::new().assert_response(
"block/467a86f0642b1d284376d13a98ef58310caa49502b0f9a560ee222e0a122fe16",
"/block/467a86f0642b1d284376d13a98ef58310caa49502b0f9a560ee222e0a122fe16",
StatusCode::NOT_FOUND,
"Not Found",
);
Expand All @@ -1047,7 +1047,7 @@ mod tests {
#[test]
fn unmined_ordinal() {
TestServer::new().assert_response_regex(
"ordinal/0",
"/ordinal/0",
StatusCode::OK,
".*<dt>time</dt><dd>2009-01-03 18:15:05</dd>.*",
);
Expand All @@ -1056,7 +1056,7 @@ mod tests {
#[test]
fn mined_ordinal() {
TestServer::new().assert_response_regex(
"ordinal/5000000000",
"/ordinal/5000000000",
StatusCode::OK,
".*<dt>time</dt><dd>.* \\(expected\\)</dd>.*",
);
Expand All @@ -1065,7 +1065,7 @@ mod tests {
#[test]
fn static_asset() {
TestServer::new().assert_response_regex(
"static/index.css",
"/static/index.css",
StatusCode::OK,
r".*\.rare \{
background-color: cornflowerblue;
Expand All @@ -1075,7 +1075,7 @@ mod tests {

#[test]
fn favicon() {
TestServer::new().assert_response_regex("favicon.ico", StatusCode::OK, r".*");
TestServer::new().assert_response_regex("/favicon.ico", StatusCode::OK, r".*");
}

#[test]
Expand All @@ -1088,6 +1088,45 @@ mod tests {

#[test]
fn clock_is_served_with_svg_extension() {
TestServer::new().assert_response_regex("clock.svg", StatusCode::OK, "<svg.*");
TestServer::new().assert_response_regex("/clock.svg", StatusCode::OK, "<svg.*");
}

#[test]
fn block() {
let test_server = TestServer::new();

test_server.bitcoin_rpc_server.broadcast_dummy_tx();
let block_hash = test_server.bitcoin_rpc_server.mine_blocks(1)[0].block_hash();

test_server.assert_response_regex(
&format!("/block/{block_hash}"),
StatusCode::OK,
".*<h1>Block [[:xdigit:]]{64}</h1>
<h2>Transactions</h2>
<ul class=monospace>
<li><a href=/tx/[[:xdigit:]]{64}>[[:xdigit:]]{64}</a></li>
<li><a href=/tx/[[:xdigit:]]{64}>[[:xdigit:]]{64}</a></li>
</ul>.*",
);
}

#[test]
fn transaction() {
let test_server = TestServer::new();

let coinbase_tx = test_server.bitcoin_rpc_server.mine_blocks(1)[0].txdata[0].clone();
let txid = coinbase_tx.txid();

test_server.assert_response_regex(
&format!("/tx/{txid}"),
StatusCode::OK,
&format!(
".*<title>Transaction {txid}</title>.*<h1>Transaction {txid}</h1>
<h2>Outputs</h2>
<ul class=monospace>
<li><a href=/output/{txid}:0>{txid}:0</a></li>
</ul>.*"
),
);
}
}
154 changes: 102 additions & 52 deletions src/test.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
use {
super::*,
bitcoin::BlockHeader,
bitcoincore_rpc::Auth,
bitcoin::{blockdata::constants::COIN_VALUE, blockdata::script, BlockHeader, TxIn, Witness},
jsonrpc_core::IoHandler,
jsonrpc_http_server::{CloseHandle, ServerBuilder},
std::collections::BTreeMap,
};

pub(crate) use {bitcoincore_rpc::RpcApi, tempfile::TempDir};
pub(crate) use tempfile::TempDir;

macro_rules! assert_regex_match {
($string:expr, $pattern:expr $(,)?) => {
let pattern: &'static str = $pattern;
let regex = Regex::new(&format!("^(?s){}$", pattern)).unwrap();
let regex = Regex::new(&format!("^(?s){}$", $pattern)).unwrap();
let string = $string;

if !regex.is_match(string.as_ref()) {
Expand All @@ -24,12 +22,14 @@ macro_rules! assert_regex_match {
};
}

struct Blocks {
struct BitcoinRpcData {
hashes: Vec<BlockHash>,
blocks: BTreeMap<BlockHash, Block>,
transactions: BTreeMap<Txid, Transaction>,
mempool: Vec<Transaction>,
}

impl Blocks {
impl BitcoinRpcData {
fn new() -> Self {
let mut hashes = Vec::new();
let mut blocks = BTreeMap::new();
Expand All @@ -39,32 +39,69 @@ impl Blocks {
hashes.push(genesis_block_hash);
blocks.insert(genesis_block_hash, genesis_block);

Self { hashes, blocks }
Self {
hashes,
blocks,
transactions: BTreeMap::new(),
mempool: Vec::new(),
}
}

fn push_block(&mut self, mut block: Block) -> BlockHash {
fn push_block(&mut self, header: BlockHeader) -> Block {
let mut block = Block {
header,
txdata: vec![Transaction {
version: 0,
lock_time: 0,
input: vec![TxIn {
previous_output: OutPoint::null(),
script_sig: script::Builder::new()
.push_scriptint(self.blocks.len().try_into().unwrap())
.into_script(),
sequence: 0,
witness: Witness::new(),
}],
output: vec![TxOut {
value: 50 * COIN_VALUE,
script_pubkey: script::Builder::new().into_script(),
}],
}],
};

block.header.prev_blockhash = *self.hashes.last().unwrap();
block.txdata.append(&mut self.mempool);

let block_hash = block.block_hash();
self.hashes.push(block_hash);
self.blocks.insert(block_hash, block);
block_hash
self.blocks.insert(block_hash, block.clone());
for tx in &block.txdata {
self.transactions.insert(tx.txid(), tx.clone());
}

block
}

fn broadcast_tx(&mut self, tx: Transaction) {
self.mempool.push(tx);
}
}

pub struct BitcoinRpcServer {
blocks: Mutex<Blocks>,
data: Arc<Mutex<BitcoinRpcData>>,
}

impl BitcoinRpcServer {
fn new() -> Self {
Self {
blocks: Mutex::new(Blocks::new()),
data: Arc::new(Mutex::new(BitcoinRpcData::new())),
}
}

pub(crate) fn spawn() -> BitcoinRpcServerHandle {
let bitcoin_rpc_server = BitcoinRpcServer::new();
let data = bitcoin_rpc_server.data.clone();
let mut io = IoHandler::default();
io.extend_with(BitcoinRpcServer::new().to_delegate());
io.extend_with(bitcoin_rpc_server.to_delegate());

let rpc_server = ServerBuilder::new(io)
.threads(1)
Expand All @@ -79,12 +116,13 @@ impl BitcoinRpcServer {
BitcoinRpcServerHandle {
close_handle: Some(close_handle),
port,
data,
}
}
}

#[jsonrpc_derive::rpc]
pub trait BitcoinRpc {
pub trait BitcoinRpcApi {
#[rpc(name = "getblockhash")]
fn getblockhash(&self, height: usize) -> Result<BlockHash, jsonrpc_core::Error>;

Expand All @@ -98,17 +136,18 @@ pub trait BitcoinRpc {
#[rpc(name = "getblock")]
fn getblock(&self, blockhash: BlockHash, verbosity: u64) -> Result<String, jsonrpc_core::Error>;

#[rpc(name = "generatetoaddress")]
fn generate_to_address(
#[rpc(name = "getrawtransaction")]
fn get_raw_transaction(
&self,
count: usize,
address: Address,
) -> Result<Vec<bitcoin::BlockHash>, jsonrpc_core::Error>;
txid: Txid,
verbose: bool,
blockhash: Option<BlockHash>,
) -> Result<String, jsonrpc_core::Error>;
}

impl BitcoinRpc for BitcoinRpcServer {
impl BitcoinRpcApi for BitcoinRpcServer {
fn getblockhash(&self, height: usize) -> Result<BlockHash, jsonrpc_core::Error> {
match self.blocks.lock().unwrap().hashes.get(height) {
match self.data.lock().unwrap().hashes.get(height) {
Some(block_hash) => Ok(*block_hash),
None => Err(jsonrpc_core::Error::new(
jsonrpc_core::types::error::ErrorCode::ServerError(-8),
Expand All @@ -122,7 +161,7 @@ impl BitcoinRpc for BitcoinRpcServer {
verbose: bool,
) -> Result<String, jsonrpc_core::Error> {
assert!(!verbose);
match self.blocks.lock().unwrap().blocks.get(&block_hash) {
match self.data.lock().unwrap().blocks.get(&block_hash) {
Some(block) => Ok(hex::encode(bitcoin::consensus::encode::serialize(
&block.header,
))),
Expand All @@ -134,59 +173,70 @@ impl BitcoinRpc for BitcoinRpcServer {

fn getblock(&self, block_hash: BlockHash, verbosity: u64) -> Result<String, jsonrpc_core::Error> {
assert_eq!(verbosity, 0, "Verbosity level {verbosity} is unsupported");
match self.blocks.lock().unwrap().blocks.get(&block_hash) {
match self.data.lock().unwrap().blocks.get(&block_hash) {
Some(block) => Ok(hex::encode(bitcoin::consensus::encode::serialize(block))),
None => Err(jsonrpc_core::Error::new(
jsonrpc_core::types::error::ErrorCode::ServerError(-8),
)),
}
}

fn generate_to_address(
fn get_raw_transaction(
&self,
count: usize,
_address: Address,
) -> Result<Vec<BlockHash>, jsonrpc_core::Error> {
let mut block_hashes = Vec::new();
let mut blocks = self.blocks.lock().unwrap();

for _ in 0..count {
block_hashes.push(blocks.push_block(Block {
header: BlockHeader {
version: 0,
prev_blockhash: BlockHash::default(),
merkle_root: Default::default(),
time: 0,
bits: 0,
nonce: 0,
},
txdata: Vec::new(),
}));
txid: Txid,
verbose: bool,
blockhash: Option<BlockHash>,
) -> Result<String, jsonrpc_core::Error> {
assert!(!verbose, "Verbose param is unsupported");
assert_eq!(blockhash, None, "Blockhash param is unsupported");
match self.data.lock().unwrap().transactions.get(&txid) {
Some(tx) => Ok(hex::encode(bitcoin::consensus::encode::serialize(tx))),
None => Err(jsonrpc_core::Error::new(
jsonrpc_core::types::error::ErrorCode::ServerError(-8),
)),
}

Ok(block_hashes)
}
}

pub(crate) struct BitcoinRpcServerHandle {
pub(crate) port: u16,
close_handle: Option<CloseHandle>,
data: Arc<Mutex<BitcoinRpcData>>,
}

impl BitcoinRpcServerHandle {
pub(crate) fn url(&self) -> String {
format!("http://127.0.0.1:{}", self.port)
}

pub(crate) fn client(&self) -> bitcoincore_rpc::Client {
bitcoincore_rpc::Client::new(&self.url(), Auth::None).unwrap()
pub(crate) fn mine_blocks(&self, num: u64) -> Vec<Block> {
let mut mined_blocks = Vec::new();
let mut bitcoin_rpc_data = self.data.lock().unwrap();
for _ in 0..num {
let block = bitcoin_rpc_data.push_block(BlockHeader {
version: 0,
prev_blockhash: BlockHash::default(),
merkle_root: Default::default(),
time: 0,
bits: 0,
nonce: 0,
});
mined_blocks.push(block);
}
mined_blocks
}

pub(crate) fn mine_blocks(&self, num: u64) {
self
.client()
.generate_to_address(num, &"1BitcoinEaterAddressDontSendf59kuE".parse().unwrap())
.unwrap();
pub(crate) fn broadcast_dummy_tx(&self) -> Txid {
let tx = Transaction {
version: 1,
lock_time: 0,
input: Vec::new(),
output: Vec::new(),
};
let txid = tx.txid();
self.data.lock().unwrap().broadcast_tx(tx);

txid
}
}

Expand Down
Loading

0 comments on commit 6d1f36f

Please sign in to comment.