use super::{
Error, get_hash_key, get_int_conf, get_plugin_factory, get_str_conf,
get_str_slice_conf,
};
use async_trait::async_trait;
use ctor::ctor;
use http::StatusCode;
use humantime::parse_duration;
use pingap_config::{PluginCategory, PluginConf};
use pingap_core::{
Ctx, HttpResponse, Plugin, PluginStep, RequestPluginResult, convert_headers,
};
use pingora::proxy::Session;
use std::borrow::Cow;
use std::sync::Arc;
use std::time::Duration;
use tokio::time::sleep;
use tracing::debug;
type Result<T, E = Error> = std::result::Result<T, E>;
pub struct MockResponse {
pub path: String,
pub plugin_step: PluginStep,
pub resp: HttpResponse,
pub delay: Option<Duration>,
hash_value: String,
}
impl MockResponse {
pub fn new(params: &PluginConf) -> Result<Self> {
debug!(params = params.to_string(), "new mock plugin");
let hash_value = get_hash_key(params);
let path = get_str_conf(params, "path"); let status = get_int_conf(params, "status") as u16; let headers = get_str_slice_conf(params, "headers"); let data = get_str_conf(params, "data");
let delay = get_str_conf(params, "delay");
let delay = if !delay.is_empty() {
let d = parse_duration(&delay).map_err(|e| Error::Invalid {
category: PluginCategory::Mock.to_string(),
message: e.to_string(),
})?;
Some(d)
} else {
None
};
let mut resp = HttpResponse {
status: StatusCode::OK, body: data.into(),
..Default::default()
};
if status > 0 {
resp.status =
StatusCode::from_u16(status).unwrap_or(StatusCode::OK);
}
if !headers.is_empty()
&& let Ok(headers) = convert_headers(&headers)
{
resp.headers = Some(headers);
}
Ok(MockResponse {
hash_value,
resp,
plugin_step: PluginStep::Request,
path,
delay,
})
}
}
#[async_trait]
impl Plugin for MockResponse {
#[inline]
fn config_key(&self) -> Cow<'_, str> {
Cow::Borrowed(&self.hash_value)
}
async fn handle_request(
&self,
step: PluginStep,
session: &mut Session,
_ctx: &mut Ctx,
) -> pingora::Result<RequestPluginResult> {
if step != self.plugin_step {
return Ok(RequestPluginResult::Skipped);
}
if !self.path.is_empty() && session.req_header().uri.path() != self.path
{
return Ok(RequestPluginResult::Skipped);
}
if let Some(d) = self.delay {
sleep(d).await;
}
Ok(RequestPluginResult::Respond(self.resp.clone()))
}
}
#[ctor]
fn init() {
get_plugin_factory()
.register("mock", |params| Ok(Arc::new(MockResponse::new(params)?)));
}
#[cfg(test)]
mod tests {
use super::*;
use bytes::Bytes;
use http::StatusCode;
use pingap_config::PluginConf;
use pingap_core::{Ctx, PluginStep};
use pingora::proxy::Session;
use pretty_assertions::assert_eq;
use tokio_test::io::Builder;
#[test]
fn test_mock_params() {
let params = MockResponse::new(
&toml::from_str::<PluginConf>(
r###"
path = "/"
status = 500
headers = [
"Content-Type: application/json"
]
data = "{\"message\":\"Mock Service Unavailable\"}"
"###,
)
.unwrap(),
)
.unwrap();
assert_eq!("/", params.path);
}
#[tokio::test]
async fn test_mock_response() {
let params = toml::from_str::<PluginConf>(
r###"
path = "/vicanso/pingap"
status = 500
headers = [
"Content-Type: application/json"
]
data = "{\"message\":\"Mock Service Unavailable\"}"
"###,
)
.unwrap();
let mock = MockResponse::new(¶ms).unwrap();
let headers = ["Accept-Encoding: gzip"].join("\r\n");
let input_header =
format!("GET /vicanso/pingap?size=1 HTTP/1.1\r\n{headers}\r\n\r\n");
let mock_io = Builder::new().read(input_header.as_bytes()).build();
let mut session = Session::new_h1(Box::new(mock_io));
session.read_request().await.unwrap();
let result = mock
.handle_request(
PluginStep::Request,
&mut session,
&mut Ctx::default(),
)
.await
.unwrap();
let RequestPluginResult::Respond(resp) = result else {
panic!("result is not Respond");
};
assert_eq!(StatusCode::INTERNAL_SERVER_ERROR, resp.status);
assert_eq!(
r###"Some([("content-type", "application/json")])"###,
format!("{:?}", resp.headers)
);
assert_eq!(
Bytes::from_static(b"{\"message\":\"Mock Service Unavailable\"}"),
resp.body
);
let headers = ["Accept-Encoding: gzip"].join("\r\n");
let input_header =
format!("GET /vicanso?size=1 HTTP/1.1\r\n{headers}\r\n\r\n");
let mock_io = Builder::new().read(input_header.as_bytes()).build();
let mut session = Session::new_h1(Box::new(mock_io));
session.read_request().await.unwrap();
let result = mock
.handle_request(
PluginStep::Request,
&mut session,
&mut Ctx::default(),
)
.await
.unwrap();
assert_eq!(true, result == RequestPluginResult::Skipped);
}
}