mod common;
use std::convert::Infallible;
use std::future::{ready, Ready};
use std::pin::Pin;
use std::task::{Context, Poll};
use axum::body::Body;
use http::StatusCode;
use prost_reflect::{DescriptorPool, DynamicMessage, MessageDescriptor};
use serde_json::{json, Value};
use structured_proxy::transcode::codec::DynamicCodec;
const ITEMS_PROTO: &str = r#"
syntax = "proto3";
package test.v1;
import "google/api/annotations.proto";
import "google/protobuf/duration.proto";
import "google/protobuf/timestamp.proto";
import "google/protobuf/wrappers.proto";
message Item {
string name = 1;
string display_name = 2;
int32 max_items = 3;
google.protobuf.Timestamp at = 4;
google.protobuf.Duration ttl = 5;
google.protobuf.StringValue note = 6;
}
service Items {
rpc Echo(Item) returns (Item) {
option (google.api.http) = {
get: "/v1/items/{name}"
additional_bindings { post: "/v1/items" body: "*" }
additional_bindings { post: "/v1/items/{name}/note" body: "note" }
};
}
}
"#;
#[derive(Clone)]
struct Echo;
impl tonic::server::UnaryService<DynamicMessage> for Echo {
type Response = DynamicMessage;
type Future = Ready<Result<tonic::Response<DynamicMessage>, tonic::Status>>;
fn call(&mut self, request: tonic::Request<DynamicMessage>) -> Self::Future {
ready(Ok(tonic::Response::new(request.into_inner())))
}
}
#[derive(Clone)]
struct Items {
item: MessageDescriptor,
}
impl tonic::server::NamedService for Items {
const NAME: &'static str = "test.v1.Items";
}
impl tower::Service<http::Request<tonic::body::Body>> for Items {
type Response = http::Response<tonic::body::Body>;
type Error = Infallible;
type Future =
Pin<Box<dyn std::future::Future<Output = Result<Self::Response, Infallible>> + Send>>;
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: http::Request<tonic::body::Body>) -> Self::Future {
let item = self.item.clone();
Box::pin(async move {
assert_eq!(req.uri().path(), "/test.v1.Items/Echo");
let mut grpc = tonic::server::Grpc::new(DynamicCodec::new(item));
Ok(grpc.unary(Echo, req).await)
})
}
}
async fn proxy() -> axum::Router {
let pool: DescriptorPool = common::compile("test/v1/items.proto", ITEMS_PROTO);
let item = pool.get_message_by_name("test.v1.Item").unwrap();
let upstream = common::serve(Items { item }).await;
common::proxy(&upstream, pool, Default::default())
}
async fn get(app: &axum::Router, uri: &str) -> (StatusCode, Value) {
let (status, body) =
common::send(app, http::Request::get(uri).body(Body::empty()).unwrap()).await;
(status, serde_json::from_str(&body).unwrap())
}
async fn post_form(app: &axum::Router, uri: &str, form: &'static str) -> (StatusCode, Value) {
let request = http::Request::post(uri)
.header("content-type", "application/x-www-form-urlencoded")
.body(Body::from(form))
.unwrap();
let (status, body) = common::send(app, request).await;
(status, serde_json::from_str(&body).unwrap())
}
#[tokio::test]
async fn query_and_form_keys_bind_by_proto_or_json_name() {
let app = proxy().await;
let (status, body) = get(&app, "/v1/items/a?displayName=Ann&max_items=3").await;
assert_eq!(status, StatusCode::OK, "{body}");
assert_eq!(
body,
json!({"name": "a", "displayName": "Ann", "maxItems": 3})
);
let (status, body) = post_form(&app, "/v1/items", "name=b&displayName=Bo&maxItems=4").await;
assert_eq!(status, StatusCode::OK, "{body}");
assert_eq!(
body,
json!({"name": "b", "displayName": "Bo", "maxItems": 4})
);
}
#[tokio::test]
async fn well_known_type_fields_bind_one_by_one() {
let app = proxy().await;
let (status, body) = get(
&app,
"/v1/items/a?at.seconds=5&at.nanos=7&ttl=1.5s¬e.value=hi",
)
.await;
assert_eq!(status, StatusCode::OK, "{body}");
assert_eq!(body["at"], "1970-01-01T00:00:05.000000007Z");
assert_eq!(body["ttl"], "1.500s");
assert_eq!(body["note"], "hi");
let (status, body) = post_form(&app, "/v1/items/c/note", "value=hello").await;
assert_eq!(status, StatusCode::OK, "{body}");
assert_eq!(body["name"], "c");
assert_eq!(body["note"], "hello");
}
#[tokio::test]
async fn an_invalid_well_known_value_is_rejected_before_the_upstream() {
let app = proxy().await;
for uri in [
"/v1/items/a?at.nanos=2000000000",
"/v1/items/a?ttl.seconds=5&ttl.nanos=-1",
] {
let (status, body) = get(&app, uri).await;
assert_eq!(status, StatusCode::BAD_REQUEST, "{uri}: {body}");
assert_eq!(body["error"], "INVALID_ARGUMENT", "{uri}");
assert_eq!(body["code"], 3, "{uri}");
}
}