Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
beltram committed Jul 1, 2023
1 parent b51c9e5 commit 5166dd8
Show file tree
Hide file tree
Showing 12 changed files with 141 additions and 75 deletions.
7 changes: 4 additions & 3 deletions attributes/src/iso.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ use quote::quote;

pub(crate) fn iso_transform(args: syn::AttributeArgs, item: TokenStream) -> syn::Result<TokenStream> {
let func = syn::parse2::<syn::ItemFn>(item)?;
let ret = &func.sig.output;
let name = &func.sig.ident;
let body = &func.block;
let attrs = &func.attrs;
Expand All @@ -16,13 +15,15 @@ pub(crate) fn iso_transform(args: syn::AttributeArgs, item: TokenStream) -> syn:
let wiremock_name = proc_macro2::Ident::new(&format!("wiremock_{}", name), name.span());
Ok(quote! {
#(#attrs)*
#vis #asyncness fn #stubr_name() #ret {
#[async_std::test]
#vis #asyncness fn #stubr_name() {
#stubr_starter
#body
}
#(#attrs)*
#[async_std::test]
#[cfg(wiremock_test)]
#vis #asyncness fn #wiremock_name() #ret {
#vis #asyncness fn #wiremock_name() {
use stubr::WiremockExt as _;
#wiremock_starter
#body
Expand Down
2 changes: 1 addition & 1 deletion attributes/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ pub fn wiremock(args: TokenStream, item: TokenStream) -> TokenStream {

#[cfg(feature = "iso")]
#[proc_macro_attribute]
pub fn iso(args: TokenStream, item: TokenStream) -> TokenStream {
pub fn iso_test(args: TokenStream, item: TokenStream) -> TokenStream {
let args = syn::parse_macro_input!(args as syn::AttributeArgs);
iso::iso_transform(args, item.into()).unwrap().into()
}
24 changes: 23 additions & 1 deletion lib/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ pub type StubrResult<T> = Result<T, StubrError>;

#[derive(thiserror::Error, Debug)]
pub enum StubrError {
#[error("Invalid stub '{0:?}'")]
#[error("Invalid stub {0:?}")]
InvalidStub(std::path::PathBuf),
#[error("Stub {0:?} not found")]
StubNotFound(std::path::PathBuf),
#[error(transparent)]
UrlError(#[from] url::ParseError),
#[error(transparent)]
Expand Down Expand Up @@ -43,6 +45,8 @@ pub enum StubrError {
IntConversionError(#[from] TryFromIntError),
#[error(transparent)]
HttpError(#[from] anyhow::Error),
#[error(transparent)]
HyperServerError(#[from] HyperError),
#[error("json path error '{0}'")]
JsonPathError(String),
#[error("A request body matcher must contain at least one matcher")]
Expand Down Expand Up @@ -77,3 +81,21 @@ impl From<StubrError> for handlebars::RenderError {
}
}
}

/// Dedicated error for building internal hyper server.
/// This has to be Send + Sync
#[derive(thiserror::Error, Debug)]
pub enum HyperError {
#[error("Internal error")]
ImplementationError,
#[error(transparent)]
HyperError(#[from] hyper::Error),
#[error(transparent)]
InvalidHeaderName(#[from] http::header::InvalidHeaderName),
#[error(transparent)]
InvalidHeaderValue(#[from] http::header::InvalidHeaderValue),
#[error(transparent)]
HttpError(#[from] http::Error),
#[error("Error with http_types")]
HttpTypesError,
}
2 changes: 1 addition & 1 deletion lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ pub use record::record_client::actix::{ActixRecord, ActixRecordMiddleware};
pub use record::standalone::StubrRecord;
pub use server::{config::Config, Stubr};
#[cfg(all(feature = "attributes", feature = "iso"))]
pub use stubr_attributes::iso;
pub use stubr_attributes::{iso_test};
#[cfg(all(feature = "record-standalone", feature = "attributes"))]
pub use stubr_attributes::record;
#[cfg(all(feature = "attributes", feature = "wiremock"))]
Expand Down
12 changes: 10 additions & 2 deletions lib/src/model/response/default.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
use crate::wiremock_rs::ResponseTemplate;
use const_format::concatcp;
use http_types::headers::SERVER;

use super::ResponseAppender;

const MATCHED_STUB_ID_HEADER: &str = "Matched-Stub-Id";

const STUBR_VERSION: &str = env!("CARGO_PKG_VERSION");
const SERVER_HEADER: &str = concatcp!("stubr(", STUBR_VERSION, ")");
const SERVER_HEADER: &str = const_format::concatcp!("stubr(", STUBR_VERSION, ")");

lazy_static! {
pub(crate) static ref VARY: http_types::headers::HeaderName = http_types::headers::VARY.try_into().unwrap();
pub(crate) static ref ACCEPT_ENCODING: http_types::headers::HeaderValue = "Accept-Encoding".try_into().unwrap();
pub(crate) static ref USER_AGENT: http_types::headers::HeaderValue = "User-Agent".try_into().unwrap();
}

pub struct WiremockIsoResponse<'a>(pub Option<&'a str>);

Expand All @@ -17,6 +22,9 @@ impl ResponseAppender for WiremockIsoResponse<'_> {
if let Some(uuid) = self.0 {
resp = resp.append_header(MATCHED_STUB_ID_HEADER, uuid);
}
if resp.http_status_code.map(|s| s.is_success()).unwrap_or_default() {
resp = resp.append_multi_header(VARY.clone(), [ACCEPT_ENCODING.clone(), USER_AGENT.clone()]);
}
resp
}
}
20 changes: 17 additions & 3 deletions lib/src/wiremock_java.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ impl WiremockImage {
const TAG: &'static str = "2.35.0";
pub const PORT: u16 = 80;
pub const MAPPINGS_DIR: &'static str = "/home/wiremock/mappings";
pub const START_MSG: &'static str = "The WireMock server is started .....";

#[allow(dead_code)]
pub fn try_run(docker: &Cli, stubs: impl Into<AnyStubs>) -> StubrResult<testcontainers::Container<'_, Self>> {
Expand All @@ -27,7 +28,7 @@ impl WiremockImage {
fn write_stubs(&self, stubs: &Vec<PathBuf>) -> StubrResult<()> {
for stub in stubs {
if !stub.exists() {
panic!("Could not find stub {stub:?}")
return Err(StubrError::StubNotFound(stub.clone()));
}
let filename = stub
.file_name()
Expand Down Expand Up @@ -66,7 +67,9 @@ impl testcontainers::Image for WiremockImage {
}

fn ready_conditions(&self) -> Vec<WaitFor> {
vec![WaitFor::seconds(10)]
vec![WaitFor::StdOutMessage {
message: Self::START_MSG.to_string(),
}]
}

fn volumes(&self) -> Box<dyn Iterator<Item = (&String, &String)> + '_> {
Expand Down Expand Up @@ -96,7 +99,18 @@ pub struct WiremockArgs;

impl testcontainers::ImageArgs for WiremockArgs {
fn into_iterator(self) -> Box<dyn Iterator<Item = String>> {
Box::new(vec!["--port".to_string(), WiremockImage::PORT.to_string(), "--verbose".to_string()].into_iter())
Box::new(
vec![
"--port".to_string(),
WiremockImage::PORT.to_string(),
"--verbose".to_string(),
"--no-request-journal".to_string(),
"--async-response-enabled".to_string(),
"--local-response-templating".to_string(),
"--disable-banner".to_string(),
]
.into_iter(),
)
}
}

Expand Down
35 changes: 19 additions & 16 deletions lib/src/wiremock_rs/grpc.rs
Original file line number Diff line number Diff line change
@@ -1,35 +1,40 @@
use crate::wiremock_rs::mock_set::MountedMockState;
use crate::wiremock_rs::{mock_server::bare_server::MockServerState, mock_set::MountedMockSet, ResponseTemplate};
use crate::{
error::HyperError,
wiremock_rs::mock_set::MountedMockState,
wiremock_rs::{mock_server::bare_server::MockServerState, mock_set::MountedMockSet, ResponseTemplate},
};
use futures_timer::Delay;
use hyper::{Body, Request};
use std::sync::Arc;
use tokio::sync::RwLock;

type HyperResult<T> = Result<T, HyperError>;

pub(crate) async fn handle_grpc(
request: Request<Body>, server_state: Arc<RwLock<MockServerState>>,
) -> Result<hyper::Response<hyper::Body>, Box<dyn std::error::Error + Send + Sync>> {
) -> HyperResult<hyper::Response<hyper::Body>> {
let wiremock_request = crate::wiremock_rs::Request::from_hyper(request).await;
let (response, delay) = server_state.write().await.handle_grpc_request(wiremock_request).await;
let (response, delay) = server_state.write().await.handle_grpc_request(wiremock_request).await?;

if let Some(delay) = delay {
delay.await;
}

Ok::<_, Box<dyn std::error::Error + Send + Sync>>(response)
HyperResult::Ok(response)
}

impl MockServerState {
pub(crate) async fn handle_grpc_request(
&mut self, request: crate::wiremock_rs::Request,
) -> (hyper::Response<hyper::Body>, Option<futures_timer::Delay>) {
) -> HyperResult<(hyper::Response<Body>, Option<Delay>)> {
self.mock_set.handle_grpc_request(request).await
}
}

impl MountedMockSet {
pub(crate) async fn handle_grpc_request(
&mut self, request: crate::wiremock_rs::Request,
) -> (hyper::Response<hyper::Body>, Option<Delay>) {
) -> HyperResult<(hyper::Response<hyper::Body>, Option<Delay>)> {
let mut response_template: Option<ResponseTemplate> = None;
self.mocks.sort_by_key(|(m, _)| m.specification.priority);
for (mock, mock_state) in &mut self.mocks {
Expand All @@ -43,15 +48,14 @@ impl MountedMockSet {
}
if let Some(response_template) = response_template {
let delay = response_template.delay().map(|d| Delay::new(d.into_owned()));
(response_template.generate_grpc_response(), delay)
Ok((response_template.generate_grpc_response()?, delay))
} else {
let default_resp = tonic::codegen::http::Response::builder()
.status(200)
.header::<_, i32>("grpc-status", tonic::Code::NotFound.into())
.header("content-type", "application/grpc")
.body(hyper::Body::from(vec![0u8; 5]))
.unwrap();
(default_resp, None)
.body(hyper::Body::from(vec![0u8; 5]))?;
Ok((default_resp, None))
}
}
}
Expand All @@ -71,15 +75,14 @@ impl ResponseTemplate {
}

/// Generate a response from the template.
pub(crate) fn generate_grpc_response(&self) -> hyper::Response<hyper::Body> {
pub(crate) fn generate_grpc_response(&self) -> HyperResult<hyper::Response<hyper::Body>> {
let body = self.body.clone().unwrap_or_else(|| vec![0u8; 5]);
let body = hyper::Body::from(body);
let code: i32 = self.grpc_status_code.unwrap().into();
tonic::codegen::http::Response::builder()
let code: i32 = self.grpc_status_code.ok_or(HyperError::ImplementationError)?.into();
Ok(tonic::codegen::http::Response::builder()
.status(200)
.header("grpc-status", code.to_string())
.header("content-type", "application/grpc")
.body(body)
.unwrap()
.body(body)?)
}
}
7 changes: 4 additions & 3 deletions lib/src/wiremock_rs/mock_server/bare_server.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use core::time::Duration;
use std::net::{SocketAddr, TcpListener, TcpStream};
use std::sync::Arc;

Expand Down Expand Up @@ -52,7 +53,7 @@ impl BareMockServer {
mock_set: MountedMockSet::new(),
received_requests,
}));
let server_address = listener.local_addr().expect("Failed to get server address.");
let server_address = listener.local_addr()?;

let server_state = state.clone();
std::thread::spawn(move || {
Expand All @@ -68,10 +69,10 @@ impl BareMockServer {
.map_err(|e| e.to_string())
});
for _ in 0..40 {
if TcpStream::connect_timeout(&server_address, std::time::Duration::from_millis(25)).is_ok() {
if TcpStream::connect_timeout(&server_address, Duration::from_millis(25)).is_ok() {
break;
}
futures_timer::Delay::new(std::time::Duration::from_millis(25)).await;
futures_timer::Delay::new(Duration::from_millis(25)).await;
}

Ok(Self {
Expand Down
46 changes: 24 additions & 22 deletions lib/src/wiremock_rs/mock_server/hyper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,41 +5,41 @@ use hyper::{
service::{make_service_fn, service_fn},
};

use crate::{wiremock_rs::mock_server::bare_server::MockServerState, StubrResult};
use crate::{error::HyperError, wiremock_rs::mock_server::bare_server::MockServerState};

type DynError = Box<dyn std::error::Error + Send + Sync>;
type HyperResult<T> = Result<T, HyperError>;

/// The actual HTTP server responding to incoming requests according to the specified mocks.
pub(super) async fn try_run_server(
listener: TcpListener, server_state: Arc<tokio::sync::RwLock<MockServerState>>, shutdown_signal: tokio::sync::oneshot::Receiver<()>,
) -> StubrResult<()> {
) -> HyperResult<()> {
let request_handler = make_service_fn(move |_| {
let server_state = server_state.clone();
async move {
Ok::<_, DynError>(service_fn(move |request: hyper::Request<hyper::Body>| {
let svc = service_fn(move |request: hyper::Request<hyper::Body>| {
let server_state = server_state.clone();
async move {
let content_type = request.headers().get("content-type").map(|v| v.as_bytes());
match content_type {
Some(b"application/grpc") => {
#[cfg(feature = "grpc")]
{
crate::wiremock_rs::grpc::handle_grpc(request, server_state).await
Ok(crate::wiremock_rs::grpc::handle_grpc(request, server_state).await?)
}
#[cfg(not(feature = "grpc"))]
{
panic!("Received a gRPC request but 'grpc' feature is not turned on")
HyperResult::Err(HyperError::ImplementationError)
}
},
_ => handle_http(request, server_state).await,
}
}
}))
});
HyperResult::Ok(svc)
}
});

let server = hyper::Server::from_tcp(listener)
.unwrap()
let server = hyper::Server::from_tcp(listener)?
.executor(LocalExec)
.serve(request_handler)
.with_graceful_shutdown(async {
Expand All @@ -54,7 +54,7 @@ pub(super) async fn try_run_server(

async fn handle_http(
request: hyper::Request<hyper::Body>, server_state: Arc<tokio::sync::RwLock<MockServerState>>,
) -> Result<hyper::Response<hyper::Body>, Box<dyn std::error::Error + Send + Sync>> {
) -> HyperResult<hyper::Response<hyper::Body>> {
let wiremock_request = crate::wiremock_rs::Request::from_hyper(request).await;
let (response, delay) = server_state.write().await.handle_request(wiremock_request).await;

Expand All @@ -70,7 +70,7 @@ async fn handle_http(
delay.await;
}

Ok::<_, DynError>(http_types_response_to_hyper_response(response).await)
http_types_response_to_hyper_response(response).await
}

// An executor that can spawn !Send futures.
Expand All @@ -87,27 +87,29 @@ where
}
}

pub(crate) async fn http_types_response_to_hyper_response(mut response: http_types::Response) -> hyper::Response<hyper::Body> {
pub(crate) async fn http_types_response_to_hyper_response(mut response: http_types::Response) -> HyperResult<hyper::Response<hyper::Body>> {
let version = response.version().map(|v| v.into()).unwrap_or_default();
let mut builder = http::response::Builder::new().status(response.status() as u16).version(version);

headers_to_hyperium_headers(response.as_mut(), builder.headers_mut().unwrap());
let hyperium_headers = builder.headers_mut().ok_or(HyperError::ImplementationError);
try_headers_to_hyperium_headers(response.as_mut(), hyperium_headers?)?;

let body_bytes = response.take_body().into_bytes().await.unwrap();
let body = hyper::Body::from(body_bytes);

builder.body(body).unwrap()
let body = response
.take_body()
.into_bytes()
.await
.map_err(|_| HyperError::HttpTypesError)?;
Ok(builder.body(body.into())?)
}

fn headers_to_hyperium_headers(headers: &mut http_types::Headers, hyperium_headers: &mut http::HeaderMap) {
fn try_headers_to_hyperium_headers(headers: &mut http_types::Headers, hyperium_headers: &mut http::HeaderMap) -> HyperResult<()> {
for (name, values) in headers {
let name = format!("{}", name).into_bytes();
let name = http::header::HeaderName::from_bytes(&name).unwrap();
let name = http::header::HeaderName::from_bytes(name.as_str().as_bytes())?;

for value in values.iter() {
let value = format!("{}", value).into_bytes();
let value = http::header::HeaderValue::from_bytes(&value).unwrap();
let value = http::header::HeaderValue::from_bytes(value.as_str().as_bytes())?;
hyperium_headers.append(&name, value);
}
}
Ok(())
}
Loading

0 comments on commit 5166dd8

Please sign in to comment.