granc/granc-core/tests/reflection_client_test.rs
Víctor Martínez 7bc2e4c0a9
refactor: separate core logic into a library crate granc-core (#16)
This pull request introduces a significant internal refactor of the `granc` project, decoupling core dynamic gRPC client logic into a new reusable library crate (`granc-core`). It also improves project organization, updates documentation, and enhances workspace configuration. The main CLI functionality is now built atop this new core, making future maintenance and extensibility easier.

**Project structure and workspace improvements:**

- Created a new crate, `granc-core`, to encapsulate all core dynamic gRPC client logic, including schema resolution, dynamic request dispatch, and reflection support. This enables potential reuse outside the CLI and clarifies project boundaries. (`granc-core/Cargo.toml`, `granc-core/src/client.rs`, [[1]](diffhunk://#diff-dd6f7ed591a1bd2577444d0079c1f56851ef74e3b9df75a86ef4af76681435f6R1-R126) [[2]](diffhunk://#diff-ddab7585cf4c860c9922ed56471bccf5804da60f0ccb174158fd31b9b82457abR1-R46) [[3]](diffhunk://#diff-46d757daaa6737f1a6247142e8abff1cb5079109e641c447e8a9793ea1f063adR1-R186)
- Updated workspace configuration in `Cargo.toml` to include `granc-core`, centralize dependency versions, and set workspace-wide package metadata for consistency. (`Cargo.toml`, [Cargo.tomlL2-R26](diffhunk://#diff-2e9d962a08321605940b5a657135052fbcef87b5e360662bb527c96d9a615542L2-R26))
- Adjusted `echo-service` and other crates to use workspace-wide settings for edition and authors. (`echo-service/Cargo.toml`, [echo-service/Cargo.tomlL3-R3](diffhunk://#diff-e74eb8a3bebf341a9bee1cdcd5cd3a50e15998db5a9df9eaf9e7aec341287b1eL3-R3))

**Documentation :**

- Added a detailed `README.md` for both the main project and the new `granc-core` library, providing clear installation, usage, and architecture guidance for users and contributors. (`README.md`, `granc-core/README.md`, [[1]](diffhunk://#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5R1-R172) [[2]](diffhunk://#diff-dd6f7ed591a1bd2577444d0079c1f56851ef74e3b9df75a86ef4af76681435f6R1-R126)

**Build and tooling updates:**

- Updated `Makefile.toml` to use workspace-wide test runs and renamed tasks/binaries for consistency with the new crate layout. (`Makefile.toml`, [[1]](diffhunk://#diff-9375fd04332c86472d7be397ef09428cb86babd8826880a5835bd1d1c1bdbc08L18-R18) [[2]](diffhunk://#diff-9375fd04332c86472d7be397ef09428cb86babd8826880a5835bd1d1c1bdbc08L45-R50)

---

**Key changes:**

**1. Core library extraction and refactor**
- Moved dynamic gRPC client logic (including `GrancClient`, request/response types, and reflection handling) into a new `granc-core` crate, decoupling it from the CLI and preparing for independent publishing. [[1]](diffhunk://#diff-ddab7585cf4c860c9922ed56471bccf5804da60f0ccb174158fd31b9b82457abR1-R46) [[2]](diffhunk://#diff-46d757daaa6737f1a6247142e8abff1cb5079109e641c447e8a9793ea1f063adR1-R186)

**2. Workspace and dependency management**
- Updated the root `Cargo.toml` to add `granc-core` as a workspace member, centralize dependency versions, and set workspace-wide metadata fields (authors, edition, license, etc.).
- Adjusted `echo-service` and new crates to inherit workspace settings for consistency.

**3. Documentation**
- Updated and added comprehensive `README.md` files for both the main project and the new core library, with installation, usage, and architecture sections. [[1]](diffhunk://#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5R1-R172) [[2]](diffhunk://#diff-dd6f7ed591a1bd2577444d0079c1f56851ef74e3b9df75a86ef4af76681435f6R1-R126)
- Introduced a `CHANGELOG.md` to document project history and recent changes.

**4. Build and CI tooling**
- Updated test and generation commands in `Makefile.toml` to reflect the new workspace structure and binary names. [[1]](diffhunk://#diff-9375fd04332c86472d7be397ef09428cb86babd8826880a5835bd1d1c1bdbc08L18-R18) [[2]](diffhunk://#diff-9375fd04332c86472d7be397ef09428cb86babd8826880a5835bd1d1c1bdbc08L45-R50)

**5. Housekeeping**
- Removed outdated or redundant files as part of the refactor. [[1]](diffhunk://#diff-06572a96a58dc510037d5efa622f9bec8519bc1beab13c9f251e97e657a9d4edL1) [[2]](diffhunk://#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5L1)

This refactor lays the groundwork for improved maintainability, easier future development, and potential wider adoption of the dynamic gRPC client logic outside the CLI.
2026-01-22 15:07:10 +01:00

138 lines
4.4 KiB
Rust

use dummy_echo_service_impl::DummyEchoService;
use echo_service::{EchoServiceServer, FILE_DESCRIPTOR_SET};
use granc_core::reflection::client::{ReflectionClient, ReflectionResolveError};
use prost_reflect::DescriptorPool;
use tonic::Code;
use tonic_reflection::server::v1::ServerReflectionServer;
mod dummy_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(DummyEchoService);
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"),
}
}