autumn-plugin-grpc 0.3.0

gRPC for Autumn: serve tonic services with health, reflection, metrics, graceful drain and layered configuration
Documentation
//! Reflection: AC5.

#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic, missing_docs)]

mod common;

use common::{boot, channel};
use tonic::Code;
use tonic_reflection::pb::v1::ServerReflectionRequest;
use tonic_reflection::pb::v1::server_reflection_client::ServerReflectionClient;
use tonic_reflection::pb::v1::server_reflection_request::MessageRequest;
use tonic_reflection::pb::v1::server_reflection_response::MessageResponse;

async fn list_services(channel: tonic::transport::Channel) -> Result<Vec<String>, tonic::Status> {
    let mut client = ServerReflectionClient::new(channel);
    let request = ServerReflectionRequest {
        host: String::new(),
        message_request: Some(MessageRequest::ListServices(String::new())),
    };
    let mut stream = client
        .server_reflection_info(tokio_stream::iter([request]))
        .await?
        .into_inner();
    let response = stream.message().await?.expect("one response");
    match response.message_response {
        Some(MessageResponse::ListServicesResponse(list)) => {
            let mut names: Vec<String> = list.service.into_iter().map(|s| s.name).collect();
            names.sort();
            Ok(names)
        }
        other => panic!("unexpected reflection response: {other:?}"),
    }
}

#[tokio::test(flavor = "multi_thread")]
async fn development_lists_user_health_and_reflection_services() {
    let (_http, handle) = boot(common::echo_plugin());
    let names = list_services(channel(&handle).await).await.unwrap();
    assert!(
        names.contains(&"autumn.echo.v1.Echo".to_owned()),
        "{names:?}"
    );
    assert!(
        names.contains(&"grpc.health.v1.Health".to_owned()),
        "{names:?}"
    );
    assert!(
        names.contains(&"grpc.reflection.v1.ServerReflection".to_owned()),
        "{names:?}"
    );
    handle.shutdown().await;
}

#[tokio::test(flavor = "multi_thread")]
async fn v1alpha_is_served_too() {
    let (_http, handle) = boot(common::echo_plugin());
    let mut client =
        tonic_reflection::pb::v1alpha::server_reflection_client::ServerReflectionClient::new(
            channel(&handle).await,
        );
    let request = tonic_reflection::pb::v1alpha::ServerReflectionRequest {
        host: String::new(),
        message_request: Some(
            tonic_reflection::pb::v1alpha::server_reflection_request::MessageRequest::ListServices(
                String::new(),
            ),
        ),
    };
    let mut stream = client
        .server_reflection_info(tokio_stream::iter([request]))
        .await
        .unwrap()
        .into_inner();
    assert!(stream.message().await.unwrap().is_some());
    handle.shutdown().await;
}

#[tokio::test(flavor = "multi_thread")]
async fn production_turns_reflection_off_by_default() {
    let (_http, handle) = boot(common::echo_plugin().development(false));
    let status = list_services(channel(&handle).await).await.unwrap_err();
    assert_eq!(status.code(), Code::Unimplemented);
    handle.shutdown().await;
}

#[tokio::test(flavor = "multi_thread")]
async fn production_can_force_reflection_on() {
    let plugin = common::echo_plugin()
        .development(false)
        .configure(|c| c.reflection = autumn_plugin_grpc::Toggle::On);
    let (_http, handle) = boot(plugin);
    let names = list_services(channel(&handle).await).await.unwrap();
    assert!(names.contains(&"autumn.echo.v1.Echo".to_owned()));
    handle.shutdown().await;
}

#[test]
fn a_corrupt_descriptor_set_aborts_boot() {
    let plugin = common::echo_plugin().file_descriptor_set(b"\xff\xff not protobuf");
    let outcome = std::thread::spawn(move || {
        std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
            let runtime = tokio::runtime::Runtime::new().unwrap();
            let _guard = runtime.enter();
            let _ = autumn_web::test::TestApp::new().plugin(plugin).build();
        }))
    })
    .join()
    .unwrap();
    assert!(outcome.is_err());
}