granc/granc-core/tests/reflection_client_test.rs
Víctor Martínez c9ef611e07
[feat] Generate markdown documentation for gRPC services (#46)
This PR implements a new subcommand `doc` that generates markdown documentation for a given gRPC service!

**Description**

For the most part, the inner logic of this subcommand is the same as the `describe`, the only thing that changes is the way that the found descriptor is transformed to a final output.

In this case, a `Packages` type has been implemented to transform a `ServiceDescriptor` into a map of `Package`s.

Each package groups all the file descriptors with the same package name (or namespace).

A `Package` contains all the necessary information for a file of documentation to be generated (All its contained services, messages and enum descriptors and its name).

The output of this command is a folder with all the generated documentation, which contains a file per protobuf package.

**Introduced the `granc-test-support` crate**

* Renamed the `echo_service` crate as `granc-test-support`, providing both the definition of a protobuf service for integration testing and a function to compile protobuffer at runtime into a file descriptor (Potentially this could be used to let users pass a folder to a proto project in addition to the server reflection and the local file descriptor options. For example, the `call` command could compile a file descriptor on the fly from a folder containing a protobuffer project before making the call to the gRPC server.

**Descriptor API Enhancements:**

* Added `name`, `full_name`, and `package_name` methods to the `Descriptor` enum to simplify access to descriptor metadata. (`granc-core/src/client/types.rs`)

**Dependency Management Improvements:**

* Added grouping for gRPC-related dependencies in `dependabot.yml` for improved automated dependency updates. (`.github/dependabot.yml`)
2026-02-06 13:16:19 +01:00

138 lines
4.4 KiB
Rust

use echo_service_impl::EchoServiceImpl;
use granc_core::reflection::client::{ReflectionClient, ReflectionResolveError};
use granc_test_support::echo_service::{EchoServiceServer, FILE_DESCRIPTOR_SET};
use prost_reflect::DescriptorPool;
use tonic::Code;
use tonic_reflection::server::v1::ServerReflectionServer;
mod echo_service_impl;
fn setup_reflection_client()
-> ReflectionClient<ServerReflectionServer<impl tonic_reflection::server::v1::ServerReflection>> {
// Configure the Reflection Service using the descriptor set from echo-service
let reflection_service = tonic_reflection::server::Builder::configure()
.register_encoded_file_descriptor_set(FILE_DESCRIPTOR_SET)
.build_v1()
.expect("Failed to setup Reflection Service");
ReflectionClient::new(reflection_service)
}
#[tokio::test]
async fn test_reflection_client_fetches_service_file_descriptor() {
let mut client = setup_reflection_client();
let fd_set = client
.file_descriptor_set_by_symbol("echo.EchoService")
.await
.expect("Failed to fetch file descriptor set by symbol");
let pool =
DescriptorPool::from_file_descriptor_set(fd_set).expect("Failed to build descriptor pool");
let service = pool
.get_service_by_name("echo.EchoService")
.expect("Failed to find service in file descriptor");
assert!(service.methods().all(|f| f.input().name() == "EchoRequest"));
assert!(
service
.methods()
.all(|f| f.output().name() == "EchoResponse")
);
let unary_method = service.methods().find(|m| m.name() == "UnaryEcho").unwrap();
let client_streaming_method = service
.methods()
.find(|m| m.name() == "ClientStreamingEcho")
.unwrap();
let server_streaming_method = service
.methods()
.find(|m| m.name() == "ServerStreamingEcho")
.unwrap();
let bidirectional_method = service
.methods()
.find(|m| m.name() == "BidirectionalEcho")
.unwrap();
assert!(
!unary_method.is_client_streaming(),
"Unary should not be client streaming"
);
assert!(
!unary_method.is_server_streaming(),
"Unary should not be server streaming"
);
// Assert Streaming Properties (Client Streaming only)
assert!(
client_streaming_method.is_client_streaming(),
"ClientStreaming MUST be client streaming"
);
assert!(
!client_streaming_method.is_server_streaming(),
"ClientStreaming should not be server streaming"
);
assert!(
!server_streaming_method.is_client_streaming(),
"ServerStreaming should not be client streaming"
);
assert!(
server_streaming_method.is_server_streaming(),
"ServerStreaming MUST be server streaming"
);
assert!(
bidirectional_method.is_client_streaming(),
"Bidirectional MUST be client streaming"
);
assert!(
bidirectional_method.is_server_streaming(),
"Bidirectional MUST be server streaming"
);
}
#[tokio::test]
async fn test_reflection_service_not_found_error() {
let mut client = setup_reflection_client();
let result: Result<_, _> = client
.file_descriptor_set_by_symbol("non.existent.Service")
.await;
assert!(matches!(
result,
Err(ReflectionResolveError::ServerStreamFailure(status)) if status.code() == Code::NotFound
));
}
#[tokio::test]
async fn test_server_does_not_support_reflection() {
// Create a server that ONLY hosts the EchoService.
// This server does NOT have the Reflection service registered.
let server = EchoServiceServer::new(EchoServiceImpl);
let mut client = ReflectionClient::new(server);
// The client will attempt to call `/grpc.reflection.v1.ServerReflection/ServerReflectionInfo` on this service.
let result = client
.file_descriptor_set_by_symbol("echo.EchoService")
.await;
match result {
Err(ReflectionResolveError::ServerStreamInitFailed(status)) => {
assert_eq!(
status.code(),
tonic::Code::Unimplemented,
"Expected UNIMPLEMENTED status (service not found), but got: {:?}",
status
);
}
Err(e) => panic!("Expected StreamInitFailed(Unimplemented), got: {:?}", e),
Ok(_) => panic!("Expected error, but got successful registry"),
}
}