granc/granc-core/tests/granc_client_test.rs
Víctor Martínez 26e46a4003
feat: implement list and describe commands (#26)
This pull request introduces major improvements to the `granc` gRPC CLI, focusing on enhanced introspection and discovery features, a more user-friendly command-line interface, and improved error and output formatting. The changes include new commands for listing and describing services, methods, and messages, a restructured CLI argument parser, and a new formatter for colored, readable output. Additionally, the core client is extended to support these new features, and error handling is refactored for clarity.

**New CLI features and UX improvements:**

* Added new `list` and `describe` commands to the CLI, allowing users to discover available services and inspect service/message definitions directly from the server using reflection. The CLI argument structure is now subcommand-based for better usability. [[1]](diffhunk://#diff-dfa67e7f5e147119fe8d665da6b31b3605f5e196734ec7407aab2bcc9e2f656cL8-R84) [[2]](diffhunk://#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5L71-R139)
* Updated the README with documentation for the new commands and improved usage instructions. [[1]](diffhunk://#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5R21) [[2]](diffhunk://#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5L71-R139)

**Core client and reflection enhancements:**

* Implemented new methods in the core client for listing services and fetching symbol descriptors via reflection, including robust error types for each operation. [[1]](diffhunk://#diff-46d757daaa6737f1a6247142e8abff1cb5079109e641c447e8a9793ea1f063adL25-R27) [[2]](diffhunk://#diff-46d757daaa6737f1a6247142e8abff1cb5079109e641c447e8a9793ea1f063adL38-R78) [[3]](diffhunk://#diff-46d757daaa6737f1a6247142e8abff1cb5079109e641c447e8a9793ea1f063adR149-R211) [[4]](diffhunk://#diff-46d757daaa6737f1a6247142e8abff1cb5079109e641c447e8a9793ea1f063adL125-R229) [[5]](diffhunk://#diff-46d757daaa6737f1a6247142e8abff1cb5079109e641c447e8a9793ea1f063adL154-R252) [[6]](diffhunk://#diff-46d757daaa6737f1a6247142e8abff1cb5079109e641c447e8a9793ea1f063adL164-R262) [[7]](diffhunk://#diff-13deee04dd97de938cc46f0ef4faca083f3b471800e94cf45937122b83f01d57R19) [[8]](diffhunk://#diff-13deee04dd97de938cc46f0ef4faca083f3b471800e94cf45937122b83f01d57R124-R161)

**Output formatting and error handling:**

* Added a new `formatter` module for producing colored, human-friendly output for all major CLI operations, including pretty-printing of service lists, descriptors, and errors.
* Improved error handling throughout the client and CLI, with more specific error types and user-facing messages. [[1]](diffhunk://#diff-46d757daaa6737f1a6247142e8abff1cb5079109e641c447e8a9793ea1f063adL38-R78) [[2]](diffhunk://#diff-0de6f761cf394791a15b0707e1a41f54559b5626f7aedb06ef339bc1a7ca6287R1-R248)

**Dependency and project structure updates:**

* Updated dependencies and added the `colored` crate for output styling.
2026-01-24 19:39:59 +01:00

213 lines
6.9 KiB
Rust

use echo_service::EchoServiceServer;
use echo_service::FILE_DESCRIPTOR_SET;
use echo_service_impl::EchoServiceImpl;
use granc_core::client::{DynamicRequest, DynamicResponse, GrancClient};
use tonic_reflection::server::v1::ServerReflectionServer;
mod echo_service_impl;
fn reflection_service()
-> ServerReflectionServer<impl tonic_reflection::server::v1::ServerReflection> {
tonic_reflection::server::Builder::configure()
.register_encoded_file_descriptor_set(FILE_DESCRIPTOR_SET)
.build_v1()
.expect("Failed to setup Reflection Service")
}
#[tokio::test]
async fn test_unary() {
let payload = serde_json::json!({ "message": "hello" });
let request = DynamicRequest {
file_descriptor_set: Some(FILE_DESCRIPTOR_SET.to_vec()),
body: payload.clone(),
headers: vec![],
service: "echo.EchoService".to_string(),
method: "UnaryEcho".to_string(),
};
let mut client = GrancClient::new(EchoServiceServer::new(EchoServiceImpl));
let res = client.dynamic(request).await.unwrap();
match res {
DynamicResponse::Unary(Ok(value)) => assert_eq!(value, payload),
DynamicResponse::Unary(Err(_)) => {
panic!("Received error status for valid unary request")
}
_ => panic!("Received stream response for unary request"),
};
}
#[tokio::test]
async fn test_server_streaming() {
let payload = serde_json::json!({ "message": "stream" });
let request = DynamicRequest {
file_descriptor_set: Some(FILE_DESCRIPTOR_SET.to_vec()),
body: payload.clone(),
headers: vec![],
service: "echo.EchoService".to_string(),
method: "ServerStreamingEcho".to_string(),
};
let mut client = GrancClient::new(EchoServiceServer::new(EchoServiceImpl));
let res = client.dynamic(request).await.unwrap();
match res {
DynamicResponse::Streaming(Ok(elems)) => {
let results: Vec<_> = elems.into_iter().map(|r| r.unwrap()).collect();
assert_eq!(results.len(), 3);
assert_eq!(results[0]["message"], "stream - seq 0");
assert_eq!(results[1]["message"], "stream - seq 1");
assert_eq!(results[2]["message"], "stream - seq 2");
}
DynamicResponse::Streaming(Err(_)) => {
panic!("Received error status for valid server streaming request")
}
_ => panic!("Received unary response for server streaming request"),
};
}
#[tokio::test]
async fn test_client_streaming() {
let payload = serde_json::json!([
{ "message": "A" },
{ "message": "B" },
{ "message": "C" }
]);
let request = DynamicRequest {
file_descriptor_set: Some(FILE_DESCRIPTOR_SET.to_vec()),
body: payload.clone(),
headers: vec![],
service: "echo.EchoService".to_string(),
method: "ClientStreamingEcho".to_string(),
};
let mut client = GrancClient::new(EchoServiceServer::new(EchoServiceImpl));
let res = client.dynamic(request).await.unwrap();
match res {
DynamicResponse::Unary(Ok(value)) => {
assert_eq!(value, serde_json::json!({"message": "ABC"}))
}
DynamicResponse::Unary(Err(_)) => {
panic!("Received error status for valid client stream request")
}
_ => panic!("Received stream response for client stream request"),
};
}
#[tokio::test]
async fn test_bidirectional_streaming() {
let payload = serde_json::json!([
{ "message": "Ping" },
{ "message": "Pong" }
]);
let request = DynamicRequest {
file_descriptor_set: Some(FILE_DESCRIPTOR_SET.to_vec()),
body: payload.clone(),
headers: vec![],
service: "echo.EchoService".to_string(),
method: "BidirectionalEcho".to_string(),
};
let mut client = GrancClient::new(EchoServiceServer::new(EchoServiceImpl));
let res = client.dynamic(request).await.unwrap();
match res {
DynamicResponse::Streaming(Ok(elems)) => {
let results: Vec<_> = elems.into_iter().map(|r| r.unwrap()).collect();
assert_eq!(results.len(), 2);
assert_eq!(results[0]["message"], "echo: Ping");
assert_eq!(results[1]["message"], "echo: Pong");
}
DynamicResponse::Streaming(Err(_)) => {
panic!("Received error status for valid bidirectional streaming request")
}
_ => panic!("Received unary response for bidirectional streaming request"),
};
}
#[tokio::test]
async fn test_list_services_success() {
let mut client = GrancClient::new(reflection_service());
let services = client
.list_services()
.await
.expect("Failed to list services");
// We expect "echo.EchoService" because we registered it.
// The list usually also includes the reflection service itself ("grpc.reflection.v1.ServerReflection").
assert!(
services.contains(&"echo.EchoService".to_string()),
"Services list did not contain 'echo.EchoService'. Found: {:?}",
services
);
}
#[tokio::test]
async fn test_get_service_descriptor_success() {
let mut client = GrancClient::new(reflection_service());
let descriptor = client
.get_descriptor_by_symbol("echo.EchoService")
.await
.expect("Failed to get service descriptor");
let descriptor = descriptor.service_descriptor().unwrap();
assert_eq!(descriptor.name(), "EchoService");
assert_eq!(descriptor.full_name(), "echo.EchoService");
// Verify methods are present
let method_names: Vec<String> = descriptor.methods().map(|m| m.name().to_string()).collect();
assert!(method_names.contains(&"UnaryEcho".to_string()));
assert!(method_names.contains(&"ServerStreamingEcho".to_string()));
assert!(method_names.contains(&"ClientStreamingEcho".to_string()));
assert!(method_names.contains(&"BidirectionalEcho".to_string()));
}
#[tokio::test]
async fn test_get_message_descriptor_success() {
let mut client = GrancClient::new(reflection_service());
let desc = client
.get_descriptor_by_symbol("echo.EchoRequest")
.await
.expect("Failed to get message descriptor");
let desc = desc.message_descriptor().unwrap();
assert_eq!(desc.name(), "EchoRequest");
assert_eq!(desc.full_name(), "echo.EchoRequest");
// Verify fields
let fields: Vec<String> = desc.fields().map(|f| f.name().to_string()).collect();
assert!(fields.contains(&"message".to_string()));
}
#[tokio::test]
async fn test_get_descriptor_not_found() {
let mut client = GrancClient::new(reflection_service());
// "echo.GhostService" does not exist in the registered descriptors.
// The reflection client should fail to find the symbol, resulting in a ResolutionError.
let err = client
.get_descriptor_by_symbol("echo.GhostService")
.await
.unwrap_err();
assert!(matches!(
err,
granc_core::client::GetDescriptorError::NotFound(_)
));
}