Skip to content

Commit

Permalink
Restructure and add mock grpc tests
Browse files Browse the repository at this point in the history
  • Loading branch information
ethanoroshiba committed Sep 27, 2024
1 parent 33dae42 commit 1273573
Show file tree
Hide file tree
Showing 11 changed files with 544 additions and 270 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/astria-grpc-mock-test/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ tokio = { workspace = true, features = [
"time",
] }
tonic.workspace = true
futures = { workspace = true }

[dev-dependencies]
astria-grpc-mock = { path = "../astria-grpc-mock" }
Expand Down
3 changes: 2 additions & 1 deletion crates/astria-grpc-mock-test/proto/health.proto
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ option java_outer_classname = "HealthProto";
option java_package = "io.grpc.health.v1";

message HealthCheckRequest {
string service = 1;
string name = 1;
string service = 2;
}

message HealthCheckResponse {
Expand Down
13 changes: 11 additions & 2 deletions crates/astria-grpc-mock-test/src/generated/grpc.health.v1.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
// This file is @generated by prost-build.
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct HealthCheckRequest {
#[prost(string, tag = "1")]
pub name: ::prost::alloc::string::String,
#[prost(string, tag = "2")]
pub service: ::prost::alloc::string::String,
}
impl ::prost::Name for HealthCheckRequest {
const NAME: &'static str = "HealthCheckRequest";
const PACKAGE: &'static str = "grpc.health.v1";
fn full_name() -> ::prost::alloc::string::String {
::prost::alloc::format!("grpc.health.v1.{}", Self::NAME)
"grpc.health.v1.HealthCheckRequest".into()
}
fn type_url() -> ::prost::alloc::string::String {
"/grpc.health.v1.HealthCheckRequest".into()
}
}
#[allow(clippy::derive_partial_eq_without_eq)]
Expand Down Expand Up @@ -67,7 +73,10 @@ impl ::prost::Name for HealthCheckResponse {
const NAME: &'static str = "HealthCheckResponse";
const PACKAGE: &'static str = "grpc.health.v1";
fn full_name() -> ::prost::alloc::string::String {
::prost::alloc::format!("grpc.health.v1.{}", Self::NAME)
"grpc.health.v1.HealthCheckResponse".into()
}
fn type_url() -> ::prost::alloc::string::String {
"/grpc.health.v1.HealthCheckResponse".into()
}
}
/// Generated client implementations.
Expand Down
17 changes: 17 additions & 0 deletions crates/astria-grpc-mock-test/src/generated/grpc.health.v1.serde.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,16 @@ impl serde::Serialize for HealthCheckRequest {
{
use serde::ser::SerializeStruct;
let mut len = 0;
if !self.name.is_empty() {
len += 1;
}
if !self.service.is_empty() {
len += 1;
}
let mut struct_ser = serializer.serialize_struct("grpc.health.v1.HealthCheckRequest", len)?;
if !self.name.is_empty() {
struct_ser.serialize_field("name", &self.name)?;
}
if !self.service.is_empty() {
struct_ser.serialize_field("service", &self.service)?;
}
Expand All @@ -23,11 +29,13 @@ impl<'de> serde::Deserialize<'de> for HealthCheckRequest {
D: serde::Deserializer<'de>,
{
const FIELDS: &[&str] = &[
"name",
"service",
];

#[allow(clippy::enum_variant_names)]
enum GeneratedField {
Name,
Service,
}
impl<'de> serde::Deserialize<'de> for GeneratedField {
Expand All @@ -50,6 +58,7 @@ impl<'de> serde::Deserialize<'de> for HealthCheckRequest {
E: serde::de::Error,
{
match value {
"name" => Ok(GeneratedField::Name),
"service" => Ok(GeneratedField::Service),
_ => Err(serde::de::Error::unknown_field(value, FIELDS)),
}
Expand All @@ -70,9 +79,16 @@ impl<'de> serde::Deserialize<'de> for HealthCheckRequest {
where
V: serde::de::MapAccess<'de>,
{
let mut name__ = None;
let mut service__ = None;
while let Some(k) = map_.next_key()? {
match k {
GeneratedField::Name => {
if name__.is_some() {
return Err(serde::de::Error::duplicate_field("name"));
}
name__ = Some(map_.next_value()?);
}
GeneratedField::Service => {
if service__.is_some() {
return Err(serde::de::Error::duplicate_field("service"));
Expand All @@ -82,6 +98,7 @@ impl<'de> serde::Deserialize<'de> for HealthCheckRequest {
}
}
Ok(HealthCheckRequest {
name: name__.unwrap_or_default(),
service: service__.unwrap_or_default(),
})
}
Expand Down
Binary file modified crates/astria-grpc-mock-test/src/generated/grpc_health_v1.bin
Binary file not shown.
271 changes: 4 additions & 267 deletions crates/astria-grpc-mock-test/tests/health/main.rs
Original file line number Diff line number Diff line change
@@ -1,270 +1,7 @@
// allow just make the tests work for now
#![allow(clippy::should_panic_without_expect)]

use std::{
net::SocketAddr,
pin::Pin,
sync::Arc,
};

use astria_grpc_mock::{
matcher,
response,
Mock,
};
use astria_grpc_mock_test::health::{
health_client::HealthClient,
health_server::{
Health,
HealthServer,
},
HealthCheckRequest,
HealthCheckResponse,
};
use tokio::{
join,
task::JoinHandle,
};
use tokio_stream::{
wrappers::TcpListenerStream,
Stream,
};
use tonic::{
transport::Server,
Request,
Response,
Status,
};

struct MockServer {
_server: JoinHandle<()>,
local_addr: SocketAddr,
mocked: astria_grpc_mock::MockServer,
}

async fn start_mock_server() -> MockServer {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let local_addr = listener.local_addr().unwrap();
let mock_server = astria_grpc_mock::MockServer::new();
let server = tokio::spawn({
let mock_server = mock_server.clone();
async move {
let _ = Server::builder()
.add_service(HealthServer::new(HealthService {
mock_server,
}))
.serve_with_incoming(TcpListenerStream::new(listener))
.await;
}
});
MockServer {
_server: server,
local_addr,
mocked: mock_server,
}
}

struct HealthService {
mock_server: astria_grpc_mock::MockServer,
}

#[tonic::async_trait]
impl Health for HealthService {
type WatchStream =
Pin<Box<dyn Stream<Item = Result<HealthCheckResponse, Status>> + Send + 'static>>;

async fn check(
self: Arc<Self>,
request: Request<HealthCheckRequest>,
) -> Result<Response<HealthCheckResponse>, Status> {
self.mock_server.handle_request("check", request).await
}

async fn watch(
self: Arc<Self>,
_request: Request<HealthCheckRequest>,
) -> Result<Response<Self::WatchStream>, Status> {
unimplemented!()
}
}

#[tokio::test]
async fn default_response_works() {
let server = start_mock_server().await;
let mut client = HealthClient::connect(format!("http://{}", server.local_addr))
.await
.unwrap();
let mock = Mock::for_rpc_given("check", matcher::message_type::<HealthCheckRequest>())
.respond_with(response::default_response::<HealthCheckResponse>());
server.mocked.register(mock).await;
let rsp = client
.check(HealthCheckRequest {
service: "helloworld".to_string(),
})
.await
.unwrap();
assert_eq!(&HealthCheckResponse::default(), rsp.get_ref());
}

#[tokio::test]
async fn constant_response_works() {
let server = start_mock_server().await;
let mut client = HealthClient::connect(format!("http://{}", server.local_addr))
.await
.unwrap();
let expected_response = HealthCheckResponse {
status: 1,
};
let mock = Mock::for_rpc_given("check", matcher::message_type::<HealthCheckRequest>())
.respond_with(response::constant_response(expected_response.clone()));
server.mocked.register(mock).await;
let rsp = client
.check(HealthCheckRequest {
service: "helloworld".to_string(),
})
.await
.unwrap();
assert_eq!(&expected_response, rsp.get_ref());
}

#[tokio::test]
async fn constant_response_expect_two_works() {
let server = start_mock_server().await;
let mut client = HealthClient::connect(format!("http://{}", server.local_addr))
.await
.unwrap();
let expected_response = HealthCheckResponse {
status: 1,
};
let mock = Mock::for_rpc_given("check", matcher::message_type::<HealthCheckRequest>())
.respond_with(response::constant_response(expected_response.clone()))
.expect(2);

let guard = server.mocked.register_as_scoped(mock).await;
let two_checks = async move {
let res_one = client
.check(HealthCheckRequest {
service: "helloworld".to_string(),
})
.await?;

let res_two = client
.check(HealthCheckRequest {
service: "helloworld".to_string(),
})
.await?;
Ok::<_, tonic::Status>((res_one, res_two))
};

let ((), res) = join!(guard.wait_until_satisfied(), two_checks);
let res = res.unwrap();
assert_eq!(&expected_response, res.0.get_ref());
assert_eq!(&expected_response, res.1.get_ref());
}

#[tokio::test]
async fn constant_response_guard_works() {
let server = start_mock_server().await;
let mut client = HealthClient::connect(format!("http://{}", server.local_addr))
.await
.unwrap();
let expected_response = HealthCheckResponse {
status: 1,
};
let mock = Mock::for_rpc_given("check", matcher::message_type::<HealthCheckRequest>())
.respond_with(response::constant_response(expected_response.clone()))
.expect(1);

let guard = server.mocked.register_as_scoped(mock).await;
let check = client.check(HealthCheckRequest {
service: "helloworld".to_string(),
});

let ((), check_res) = join!(guard.wait_until_satisfied(), check);
let rsp = check_res.unwrap();
assert_eq!(&expected_response, rsp.get_ref());
}

#[tokio::test]
async fn exact_pbjson_match_works() {
let server = start_mock_server().await;
let mut client = HealthClient::connect(format!("http://{}", server.local_addr))
.await
.unwrap();
let expected_request = HealthCheckRequest {
service: "helloworld".to_string(),
};
let expected_response = HealthCheckResponse {
status: 1,
};
let mock = Mock::for_rpc_given("check", matcher::message_exact_pbjson(&expected_request))
.respond_with(response::constant_response(expected_response.clone()));
server.mocked.register(mock).await;
let rsp = client
.check(HealthCheckRequest {
service: "helloworld".to_string(),
})
.await
.unwrap();
assert_eq!(&expected_response, rsp.get_ref());
}

#[tokio::test]
async fn partial_pbjson_match_works() {
let server = start_mock_server().await;
let mut client = HealthClient::connect(format!("http://{}", server.local_addr))
.await
.unwrap();
let expected_request = HealthCheckRequest {
service: "helloworld".to_string(),
};
let expected_response = HealthCheckResponse {
status: 1,
};
// FIXME: Right now this is equivalent to an exact check because the request only has one field.
let mock = Mock::for_rpc_given("check", matcher::message_partial_pbjson(&expected_request))
.respond_with(response::constant_response(expected_response.clone()));
server.mocked.register(mock).await;
let rsp = client
.check(HealthCheckRequest {
service: "helloworld".to_string(),
})
.await
.unwrap();
assert_eq!(&expected_response, rsp.get_ref());
}

#[tokio::test]
#[should_panic]
async fn incorrect_mock_response_fails_server() {
let server = start_mock_server().await;
let mut client = HealthClient::connect(format!("http://{}", server.local_addr))
.await
.unwrap();
let mock = Mock::for_rpc_given("check", matcher::message_type::<HealthCheckRequest>())
.respond_with(response::default_response::<HealthCheckRequest>());
server.mocked.register(mock).await;
let _ = client
.check(HealthCheckRequest {
service: "helloworld".to_string(),
})
.await;
}

#[tokio::test]
#[should_panic]
async fn incorrect_mock_response_fails_guard() {
let server = start_mock_server().await;
let mut client = HealthClient::connect(format!("http://{}", server.local_addr))
.await
.unwrap();
let mock = Mock::for_rpc_given("check", matcher::message_type::<HealthCheckRequest>())
.respond_with(response::default_response::<HealthCheckRequest>());

let guard = server.mocked.register_as_scoped(mock).await;
let check = client.check(HealthCheckRequest {
service: "helloworld".to_string(),
});

let _ = join!(guard.wait_until_satisfied(), check);
}
mod test_matcher;
mod test_mock;
mod test_response;
mod test_utils;
Loading

0 comments on commit 1273573

Please sign in to comment.