use std::{sync::Arc, time::Duration};
use futures::StreamExt;
use tokio::{sync::broadcast, task::JoinHandle};
use tower::BoxError;
use zakura_chain::{
block::{Block, Height},
chain_tip::mock::{MockChainTip, MockChainTipSender},
serialization::ZcashDeserializeInto,
transaction::{self, UnminedTxId},
};
use zakura_node_services::mempool::{MempoolChange, MempoolTxSubscriber};
use zakura_state::{HashOrHeight, ReadRequest, ReadResponse};
use zakura_test::{
mock_service::{MockService, PanicAssertion},
prelude::color_eyre::{eyre::eyre, Result},
};
use crate::indexer::{self, indexer_client::IndexerClient, BlockRequest, Empty};
#[tokio::test]
async fn rpc_server_spawn() -> Result<()> {
let _init_guard = zakura_test::init();
let (
_server_task,
client,
mock_read_service,
mock_chain_tip_sender,
mempool_transaction_sender,
) = start_server_and_get_client().await?;
test_chain_tip_change(client.clone(), mock_chain_tip_sender).await?;
test_mempool_change(client.clone(), mempool_transaction_sender).await?;
test_get_block(client.clone(), mock_read_service).await?;
Ok(())
}
async fn test_get_block(
mut client: IndexerClient<tonic::transport::Channel>,
mut mock_read_service: MockService<ReadRequest, ReadResponse, PanicAssertion, BoxError>,
) -> Result<()> {
let status = client
.get_block(tonic::Request::new(BlockRequest {
hash_or_height: Vec::new(),
}))
.await
.expect_err("a block request without a valid hash or height should be rejected");
assert_eq!(status.code(), tonic::Code::InvalidArgument);
let status = client
.get_block(tonic::Request::new(BlockRequest {
hash_or_height: u32::MAX.to_be_bytes().to_vec(),
}))
.await
.expect_err("an out-of-range block height should be rejected");
assert_eq!(status.code(), tonic::Code::InvalidArgument);
let block: Arc<Block> = zakura_test::vectors::BLOCK_MAINNET_1_BYTES.zcash_deserialize_into()?;
let expected_hash = block.hash();
let height = block
.coinbase_height()
.expect("test block has a coinbase height");
let mut request_client = client.clone();
let request_task = tokio::spawn(async move {
request_client
.get_block(tonic::Request::new(BlockRequest {
hash_or_height: height.0.to_be_bytes().to_vec(),
}))
.await
});
mock_read_service
.expect_request(ReadRequest::Block(HashOrHeight::Height(height)))
.await
.respond(ReadResponse::Block(Some(block.clone())));
let response = request_task
.await?
.expect("get_block should succeed")
.into_inner();
let (decoded_block, decoded_hash) = response.decode().expect("response should decode");
assert_eq!(decoded_hash, expected_hash);
assert_eq!(decoded_block.hash(), expected_hash);
Ok(())
}
async fn test_chain_tip_change(
mut client: IndexerClient<tonic::transport::Channel>,
mock_chain_tip_sender: MockChainTipSender,
) -> Result<()> {
let request = tonic::Request::new(Empty {});
let mut response = client.chain_tip_change(request).await?.into_inner();
mock_chain_tip_sender.send_best_tip_height(Height::MIN);
mock_chain_tip_sender.send_best_tip_hash(zakura_chain::block::Hash([0; 32]));
tokio::time::sleep(Duration::from_millis(500)).await;
tokio::time::timeout(Duration::from_secs(3), response.next())
.await
.expect("should receive chain tip change notification before timeout")
.expect("response stream should not be empty")
.expect("chain tip change response should not be an error message");
Ok(())
}
async fn test_mempool_change(
mut client: IndexerClient<tonic::transport::Channel>,
mempool_transaction_sender: tokio::sync::broadcast::Sender<MempoolChange>,
) -> Result<()> {
let request = tonic::Request::new(Empty {});
let mut response = client.mempool_change(request).await?.into_inner();
let change_tx_ids = [UnminedTxId::Legacy(transaction::Hash::from([0; 32]))]
.into_iter()
.collect();
mempool_transaction_sender
.send(MempoolChange::added(change_tx_ids))
.expect("rpc server should have a receiver");
tokio::time::timeout(Duration::from_secs(3), response.next())
.await
.expect("should receive chain tip change notification before timeout")
.expect("response stream should not be empty")
.expect("chain tip change response should not be an error message");
Ok(())
}
async fn start_server_and_get_client() -> Result<(
JoinHandle<Result<(), BoxError>>,
IndexerClient<tonic::transport::Channel>,
MockService<ReadRequest, ReadResponse, PanicAssertion, BoxError>,
MockChainTipSender,
broadcast::Sender<MempoolChange>,
)> {
let listen_addr: std::net::SocketAddr = "127.0.0.1:0"
.parse()
.expect("hard-coded IP and u16 port should parse successfully");
let mock_read_service = MockService::build()
.with_max_request_delay(Duration::from_secs(2))
.for_unit_tests();
let (mock_chain_tip_change, mock_chain_tip_change_sender) = MockChainTip::new();
let (mempool_transaction_sender, _) = tokio::sync::broadcast::channel(1);
let mempool_tx_subscriber = MempoolTxSubscriber::new(mempool_transaction_sender.clone());
let (server_task, listen_addr) = indexer::server::init(
listen_addr,
mock_read_service.clone(),
mock_chain_tip_change,
mempool_tx_subscriber.clone(),
)
.await
.map_err(|err| eyre!(err))?;
tokio::time::sleep(Duration::from_secs(1)).await;
let endpoint = tonic::transport::channel::Endpoint::new(format!("http://{listen_addr}"))
.unwrap()
.timeout(Duration::from_secs(2));
let client = IndexerClient::connect(endpoint)
.await
.expect("server should receive connection");
Ok((
server_task,
client,
mock_read_service,
mock_chain_tip_change_sender,
mempool_transaction_sender,
))
}