tests(grpc): Unit tests (#8293)

* add grpc method unit tests

* add test module description

* runs tests in sequence, adds some messages to assertions, and minor cleanups (#8296)

* fix field name change

---------

Co-authored-by: Arya <aryasolhi@gmail.com>
This commit is contained in:
Alfredo Garcia 2024-02-20 19:45:55 -03:00 committed by GitHub
parent a7de137a7a
commit 2f0b284997
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 624 additions and 1 deletions

View File

@ -22,6 +22,9 @@ use crate::scanner::{
type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
/// The maximum number of keys that can be requested in a single request.
pub const MAX_KEYS_PER_REQUEST: usize = 10;
/// The maximum number of messages that can be queued to be streamed to a client
/// from the `scan` method.
const SCAN_RESPONDER_BUFFER_SIZE: usize = 10_000;
@ -174,13 +177,26 @@ where
&self,
request: Request<RegisterKeysRequest>,
) -> Result<Response<RegisterKeysResponse>, Status> {
let keys = request
let keys: Vec<_> = request
.into_inner()
.keys
.into_iter()
.map(|key_with_height| (key_with_height.key, key_with_height.height))
.collect();
if keys.is_empty() {
let msg = "must provide at least 1 key for which to register keys";
return Err(Status::invalid_argument(msg));
}
if keys.len() > MAX_KEYS_PER_REQUEST {
let msg = format!(
"must provide at most {} keys to register keys",
MAX_KEYS_PER_REQUEST
);
return Err(Status::invalid_argument(msg));
}
let ScanServiceResponse::RegisteredKeys(keys) = self
.scan_service
.clone()
@ -208,6 +224,14 @@ where
return Err(Status::invalid_argument(msg));
}
if keys.len() > MAX_KEYS_PER_REQUEST {
let msg = format!(
"must provide at most {} keys to clear results",
MAX_KEYS_PER_REQUEST
);
return Err(Status::invalid_argument(msg));
}
let ScanServiceResponse::ClearedResults = self
.scan_service
.clone()
@ -235,6 +259,14 @@ where
return Err(Status::invalid_argument(msg));
}
if keys.len() > MAX_KEYS_PER_REQUEST {
let msg = format!(
"must provide at most {} keys to delete",
MAX_KEYS_PER_REQUEST
);
return Err(Status::invalid_argument(msg));
}
let ScanServiceResponse::DeletedKeys = self
.scan_service
.clone()
@ -262,6 +294,14 @@ where
return Err(Status::invalid_argument(msg));
}
if keys.len() > MAX_KEYS_PER_REQUEST {
let msg = format!(
"must provide at most {} keys to get results",
MAX_KEYS_PER_REQUEST
);
return Err(Status::invalid_argument(msg));
}
let ScanServiceResponse::Results(response) = self
.scan_service
.clone()

View File

@ -1 +1,2 @@
mod snapshot;
mod vectors;

View File

@ -0,0 +1,582 @@
//! Unit tests for gRPC methods
use std::{collections::BTreeMap, thread::sleep, time::Duration};
use tonic::transport::Channel;
use zebra_chain::{block::Height, parameters::Network, transaction};
use zebra_test::{
mock_service::{MockService, PanicAssertion},
net::random_known_port,
};
use crate::{
scanner::{
scanner_client::ScannerClient, ClearResultsRequest, DeleteKeysRequest, Empty,
GetResultsRequest, GetResultsResponse, InfoReply, KeyWithHeight, RegisterKeysRequest,
RegisterKeysResponse,
},
server::{init, MAX_KEYS_PER_REQUEST},
};
use zebra_node_services::scan_service::{
request::Request as ScanRequest, response::Response as ScanResponse,
};
/// The extended Sapling viewing key of [ZECpages](https://zecpages.com/boardinfo)
pub const ZECPAGES_SAPLING_VIEWING_KEY: &str = "zxviews1q0duytgcqqqqpqre26wkl45gvwwwd706xw608hucmvfalr759ejwf7qshjf5r9aa7323zulvz6plhttp5mltqcgs9t039cx2d09mgq05ts63n8u35hyv6h9nc9ctqqtue2u7cer2mqegunuulq2luhq3ywjcz35yyljewa4mgkgjzyfwh6fr6jd0dzd44ghk0nxdv2hnv4j5nxfwv24rwdmgllhe0p8568sgqt9ckt02v2kxf5ahtql6s0ltjpkckw8gtymxtxuu9gcr0swvz";
/// Test the gRPC methods with mocked responses
#[tokio::test(flavor = "multi_thread")]
async fn test_grpc_methods_mocked() {
let _init_guard = zebra_test::init();
let (client, mock_scan_service) = start_server_and_get_client(random_known_port()).await;
test_get_results_errors(client.clone()).await;
test_register_keys_errors(client.clone()).await;
test_clear_results_errors(client.clone()).await;
test_delete_keys_errors(client.clone()).await;
for network in Network::iter() {
test_mocked_getinfo_for_network(&client, &mock_scan_service, network).await;
test_mocked_getresults_for_network(&client, &mock_scan_service, network).await;
test_mocked_register_keys_for_network(&client, &mock_scan_service, network).await;
test_mocked_clear_results_for_network(&client, &mock_scan_service, network).await;
test_mocked_delete_keys_for_network(&client, &mock_scan_service, network).await;
}
}
/// Test the `get_info` gRPC method with a mocked service response.
async fn test_mocked_getinfo_for_network(
client: &ScannerClient<Channel>,
mock_scan_service: &MockService<ScanRequest, ScanResponse, PanicAssertion>,
network: Network,
) {
// create request, fake results and get response
let get_info_response = call_get_info(client.clone(), mock_scan_service.clone(), network).await;
// test the response
assert_eq!(
get_info_response.into_inner().min_sapling_birthday_height,
network.sapling_activation_height().0,
"get_info response min sapling height should match network sapling activation height"
);
}
/// Test the `get_results` gRPC method with populated and empty results
async fn test_mocked_getresults_for_network(
client: &ScannerClient<Channel>,
mock_scan_service: &MockService<ScanRequest, ScanResponse, PanicAssertion>,
network: Network,
) {
// create request, fake populated results and get response
let get_results_response =
call_get_results(client.clone(), mock_scan_service.clone(), network, false).await;
// test the response
let transaction_heights = get_results_response
.into_inner()
.results
.first_key_value()
.expect("should have at least 1 value")
.1
.by_height
.len();
match network {
Network::Mainnet => {
assert_eq!(
transaction_heights, 3,
"there should be 3 transaction heights"
);
}
Network::Testnet => {
assert_eq!(
transaction_heights, 1,
"there should be 1 transaction height"
);
}
}
// create request, fake empty results and get response
let get_results_response =
call_get_results(client.clone(), mock_scan_service.clone(), network, true).await;
// test the response
let is_results_empty = get_results_response
.into_inner()
.results
.first_key_value()
.expect("should have at least 1 value")
.1
.by_height
.is_empty();
assert!(is_results_empty, "results should be empty");
}
/// Test the `register_keys` gRPC method
async fn test_mocked_register_keys_for_network(
client: &ScannerClient<Channel>,
mock_scan_service: &MockService<ScanRequest, ScanResponse, PanicAssertion>,
network: Network,
) {
// create request, fake return value and get response
let register_keys_response =
call_register_keys(client.clone(), mock_scan_service.clone(), network).await;
// test the response
assert_eq!(
register_keys_response.into_inner().keys,
vec![ZECPAGES_SAPLING_VIEWING_KEY.to_string()],
"keys should match mocked response"
);
}
/// Test the `clear_results` gRPC method
async fn test_mocked_clear_results_for_network(
client: &ScannerClient<Channel>,
mock_scan_service: &MockService<ScanRequest, ScanResponse, PanicAssertion>,
network: Network,
) {
// create request, fake results and get response
let get_results_response =
call_get_results(client.clone(), mock_scan_service.clone(), network, false).await;
// test the response
let transaction_heights = get_results_response
.into_inner()
.results
.first_key_value()
.unwrap()
.1
.by_height
.len();
match network {
Network::Mainnet => {
assert_eq!(transaction_heights, 3);
}
Network::Testnet => {
assert_eq!(transaction_heights, 1);
}
}
// create request, fake results and get response
let clear_results_response =
call_clear_results(client.clone(), mock_scan_service.clone(), network).await;
// test the response
assert_eq!(clear_results_response.into_inner(), Empty {});
// create request, fake results and get response
let get_results_response =
call_get_results(client.clone(), mock_scan_service.clone(), network, true).await;
// test the response
let is_results_empty = get_results_response
.into_inner()
.results
.first_key_value()
.unwrap()
.1
.by_height
.is_empty();
assert!(is_results_empty, "results should be empty");
}
/// Test the `delete_keys` gRPC method
async fn test_mocked_delete_keys_for_network(
client: &ScannerClient<Channel>,
mock_scan_service: &MockService<ScanRequest, ScanResponse, PanicAssertion>,
network: Network,
) {
// create request, fake results and get response
let register_keys_response =
call_register_keys(client.clone(), mock_scan_service.clone(), network).await;
// test the response
assert_eq!(
register_keys_response.into_inner().keys,
vec![ZECPAGES_SAPLING_VIEWING_KEY.to_string()]
);
// create request, fake results and get response
let get_results_response =
call_get_results(client.clone(), mock_scan_service.clone(), network, false).await;
let transaction_heights = get_results_response
.into_inner()
.results
.first_key_value()
.unwrap()
.1
.by_height
.len();
match network {
Network::Mainnet => {
assert_eq!(transaction_heights, 3);
}
Network::Testnet => {
assert_eq!(transaction_heights, 1);
}
}
let delete_keys_response =
call_delete_keys(client.clone(), mock_scan_service.clone(), network).await;
// test the response
assert_eq!(delete_keys_response.into_inner(), Empty {});
let get_results_response =
call_get_results(client.clone(), mock_scan_service.clone(), network, true).await;
let is_results_empty = get_results_response
.into_inner()
.results
.first_key_value()
.unwrap()
.1
.by_height
.is_empty();
assert!(is_results_empty, "results should be empty");
}
/// Start the gRPC server, get a client and a mock service
async fn start_server_and_get_client(
random_port: u16,
) -> (
ScannerClient<tonic::transport::Channel>,
MockService<ScanRequest, ScanResponse, PanicAssertion>,
) {
// get a mocked scan service
let mock_scan_service = MockService::build().for_unit_tests();
// start the gRPC server
let listen_addr: std::net::SocketAddr = format!("127.0.0.1:{random_port}")
.parse()
.expect("hard-coded IP and u16 port should parse successfully");
{
let mock_scan_service = mock_scan_service.clone();
tokio::spawn(async move {
init(listen_addr, mock_scan_service)
.await
.expect("Possible port conflict");
});
}
// wait for the server to start
sleep(Duration::from_secs(1));
// connect to the gRPC server
let client = ScannerClient::connect(format!("http://127.0.0.1:{random_port}"))
.await
.expect("server should receive connection");
(client, mock_scan_service)
}
/// Add fake populated results to the mock scan service
async fn add_fake_populated_results(
mock_scan_service: MockService<ScanRequest, ScanResponse, PanicAssertion>,
network: Network,
) {
let mut mock_scan_service = mock_scan_service.clone();
tokio::spawn(async move {
let zec_pages_sapling_efvk = ZECPAGES_SAPLING_VIEWING_KEY.to_string();
let mut fake_results = BTreeMap::new();
let heights = match network {
Network::Mainnet => vec![Height::MIN, Height(1), Height::MAX],
Network::Testnet => vec![Height::MIN],
};
for fake_result_height in heights {
fake_results.insert(
fake_result_height,
[transaction::Hash::from([0; 32])].repeat(3),
);
}
let mut fake_results_response = BTreeMap::new();
fake_results_response.insert(zec_pages_sapling_efvk, fake_results);
mock_scan_service
.expect_request_that(|req| matches!(req, ScanRequest::Results(_)))
.await
.respond(ScanResponse::Results(fake_results_response))
});
}
/// Add fake empty results to the mock scan service
async fn add_fake_empty_results(
mock_scan_service: MockService<ScanRequest, ScanResponse, PanicAssertion>,
_network: Network,
) {
let mut mock_scan_service = mock_scan_service.clone();
tokio::spawn(async move {
let zec_pages_sapling_efvk = ZECPAGES_SAPLING_VIEWING_KEY.to_string();
let fake_results = BTreeMap::new();
let mut fake_results_response = BTreeMap::new();
fake_results_response.insert(zec_pages_sapling_efvk, fake_results);
mock_scan_service
.expect_request_that(|req| matches!(req, ScanRequest::Results(_)))
.await
.respond(ScanResponse::Results(fake_results_response))
});
}
/// Call the `get_results` gRPC method, mock and return the response
async fn call_get_results(
client: ScannerClient<Channel>,
mock_scan_service: MockService<ScanRequest, ScanResponse, PanicAssertion>,
network: Network,
empty_results: bool,
) -> tonic::Response<GetResultsResponse> {
let get_results_response_fut = {
let mut client = client.clone();
let get_results_request = tonic::Request::new(GetResultsRequest {
keys: vec![ZECPAGES_SAPLING_VIEWING_KEY.to_string()],
});
tokio::spawn(async move { client.get_results(get_results_request).await })
};
if empty_results {
add_fake_empty_results(mock_scan_service, network).await;
} else {
add_fake_populated_results(mock_scan_service, network).await;
}
get_results_response_fut
.await
.expect("tokio task should join successfully")
.expect("get_results request should succeed")
}
/// Calls the `get_results` gRPC method with bad request data and asserts that it returns errors
async fn test_get_results_errors(mut client: ScannerClient<Channel>) {
let request = tonic::Request::new(GetResultsRequest { keys: vec![] });
let response = client.get_results(request).await;
let response_error_code = response
.expect_err("calling get_results with no keys should return an error")
.code();
assert_eq!(
response_error_code,
tonic::Code::InvalidArgument,
"error code should be an invalid argument error"
);
let request = tonic::Request::new(GetResultsRequest {
keys: vec![
ZECPAGES_SAPLING_VIEWING_KEY.to_string();
MAX_KEYS_PER_REQUEST
.checked_add(1)
.expect("should fit in usize")
],
});
let response = client.get_results(request).await;
let response_error_code = response
.expect_err("calling get_results too with many keys should return an error")
.code();
assert_eq!(
response_error_code,
tonic::Code::InvalidArgument,
"error code should be an invalid argument error"
);
}
/// Call the `get_info` gRPC method, mock and return the response
async fn call_get_info(
client: ScannerClient<Channel>,
mock_scan_service: MockService<ScanRequest, ScanResponse, PanicAssertion>,
network: Network,
) -> tonic::Response<InfoReply> {
let get_info_response_fut = {
let mut client = client.clone();
let get_info_request = tonic::Request::new(Empty {});
tokio::spawn(async move { client.get_info(get_info_request).await })
};
let mut mock_scan_service = mock_scan_service.clone();
tokio::spawn(async move {
mock_scan_service
.expect_request_that(|req| matches!(req, ScanRequest::Info))
.await
.respond(ScanResponse::Info {
min_sapling_birthday_height: network.sapling_activation_height(),
})
});
get_info_response_fut
.await
.expect("tokio task should join successfully")
.expect("get_info request should succeed")
}
/// Call the `register_keys` gRPC method, mock and return the response
async fn call_register_keys(
client: ScannerClient<Channel>,
mock_scan_service: MockService<ScanRequest, ScanResponse, PanicAssertion>,
_network: Network,
) -> tonic::Response<RegisterKeysResponse> {
let key_with_height = KeyWithHeight {
key: ZECPAGES_SAPLING_VIEWING_KEY.to_string(),
height: None,
};
let register_keys_response_fut = {
let mut client = client.clone();
let register_keys_request = tonic::Request::new(RegisterKeysRequest {
keys: vec![key_with_height],
});
tokio::spawn(async move { client.register_keys(register_keys_request).await })
};
let mut mock_scan_service = mock_scan_service.clone();
tokio::spawn(async move {
mock_scan_service
.expect_request_that(|req| matches!(req, ScanRequest::RegisterKeys(_)))
.await
.respond(ScanResponse::RegisteredKeys(vec![
ZECPAGES_SAPLING_VIEWING_KEY.to_string(),
]))
});
register_keys_response_fut
.await
.expect("tokio task should join successfully")
.expect("register_keys request should succeed")
}
/// Calls the `register_keys` gRPC method with bad request data and asserts that it returns errors
async fn test_register_keys_errors(client: ScannerClient<Channel>) {
let key_with_height = KeyWithHeight {
key: ZECPAGES_SAPLING_VIEWING_KEY.to_string(),
height: None,
};
let fut = {
let mut client = client.clone();
let request = tonic::Request::new(RegisterKeysRequest { keys: vec![] });
tokio::spawn(async move { client.register_keys(request).await })
};
let response = fut.await.expect("tokio task should join successfully");
assert!(response.is_err());
assert_eq!(response.err().unwrap().code(), tonic::Code::InvalidArgument);
let fut = {
let mut client = client.clone();
let request = tonic::Request::new(RegisterKeysRequest {
keys: vec![key_with_height; 11],
});
tokio::spawn(async move { client.register_keys(request).await })
};
let response = fut.await.expect("tokio task should join successfully");
assert!(response.is_err());
assert_eq!(response.err().unwrap().code(), tonic::Code::InvalidArgument);
}
async fn call_clear_results(
client: ScannerClient<Channel>,
mock_scan_service: MockService<ScanRequest, ScanResponse, PanicAssertion>,
_network: Network,
) -> tonic::Response<Empty> {
let clear_results_response_fut = {
let mut client = client.clone();
let clear_results_request = tonic::Request::new(ClearResultsRequest {
keys: vec![ZECPAGES_SAPLING_VIEWING_KEY.to_string()],
});
tokio::spawn(async move { client.clear_results(clear_results_request).await })
};
let mut mock_scan_service = mock_scan_service.clone();
tokio::spawn(async move {
mock_scan_service
.expect_request_that(|req| matches!(req, ScanRequest::ClearResults(_)))
.await
.respond(ScanResponse::ClearedResults)
});
clear_results_response_fut
.await
.expect("tokio task should join successfully")
.expect("register_keys request should succeed")
}
/// Calls the `clear_results` gRPC method with bad request data and asserts that it returns errors
async fn test_clear_results_errors(client: ScannerClient<Channel>) {
let fut = {
let mut client = client.clone();
let request = tonic::Request::new(ClearResultsRequest { keys: vec![] });
tokio::spawn(async move { client.clear_results(request).await })
};
let response = fut.await.expect("tokio task should join successfully");
assert!(response.is_err());
assert_eq!(response.err().unwrap().code(), tonic::Code::InvalidArgument);
let fut = {
let mut client = client.clone();
let request = tonic::Request::new(ClearResultsRequest {
keys: vec![ZECPAGES_SAPLING_VIEWING_KEY.to_string(); 11],
});
tokio::spawn(async move { client.clear_results(request).await })
};
let response = fut.await.expect("tokio task should join successfully");
assert!(response.is_err());
assert_eq!(response.err().unwrap().code(), tonic::Code::InvalidArgument);
}
/// Call the `delete_keys` gRPC method, mock and return the response
async fn call_delete_keys(
client: ScannerClient<Channel>,
mock_scan_service: MockService<ScanRequest, ScanResponse, PanicAssertion>,
_network: Network,
) -> tonic::Response<Empty> {
let delete_keys_response_fut = {
let mut client = client.clone();
let delete_keys_request = tonic::Request::new(DeleteKeysRequest {
keys: vec![ZECPAGES_SAPLING_VIEWING_KEY.to_string()],
});
tokio::spawn(async move { client.delete_keys(delete_keys_request).await })
};
let mut mock_scan_service = mock_scan_service.clone();
tokio::spawn(async move {
mock_scan_service
.expect_request_that(|req| matches!(req, ScanRequest::DeleteKeys(_)))
.await
.respond(ScanResponse::DeletedKeys)
});
delete_keys_response_fut
.await
.expect("tokio task should join successfully")
.expect("delete_keys request should succeed")
}
/// Calls the `delete_keys` gRPC method with bad request data and asserts that it returns errors
async fn test_delete_keys_errors(client: ScannerClient<Channel>) {
let fut = {
let mut client = client.clone();
let request = tonic::Request::new(DeleteKeysRequest { keys: vec![] });
tokio::spawn(async move { client.delete_keys(request).await })
};
let response = fut.await.expect("tokio task should join successfully");
assert!(response.is_err());
assert_eq!(response.err().unwrap().code(), tonic::Code::InvalidArgument);
let fut = {
let mut client = client.clone();
let request = tonic::Request::new(DeleteKeysRequest {
keys: vec![ZECPAGES_SAPLING_VIEWING_KEY.to_string(); 11],
});
tokio::spawn(async move { client.delete_keys(request).await })
};
let response = fut.await.expect("tokio task should join successfully");
assert!(response.is_err());
assert_eq!(response.err().unwrap().code(), tonic::Code::InvalidArgument);
}