use bytes::Bytes;
use hitbox::predicate::{Predicate, PredicateResult};
use hitbox_http::predicates::NeutralRequestPredicate;
use hitbox_http::predicates::request::BodyPredicate;
use hitbox_http::predicates::request::body::{JqExpression, JqOperation, Operation};
use hitbox_http::{BufferedBody, CacheableHttpRequest};
use http::Request;
use serde_json::json;
#[cfg(test)]
mod eq_tests {
use super::*;
use bytes::Bytes;
use http_body_util::Full;
#[tokio::test]
async fn test_positive() {
let json_body = r#"{"field":"test-value"}"#;
let body = Full::new(Bytes::from(json_body));
let request = Request::builder()
.body(BufferedBody::Passthrough(body))
.unwrap();
let request = CacheableHttpRequest::from_request(request);
let filter = JqExpression::compile(".field").unwrap();
let predicate = NeutralRequestPredicate::new().body(Operation::Jq {
filter,
operation: JqOperation::Eq("test-value".into()),
});
let prediction = predicate.check(request).await;
assert!(matches!(prediction, PredicateResult::Cacheable(_)));
}
#[tokio::test]
async fn test_negative() {
let json_body = r#"{"field":"test-value"}"#;
let body = Full::new(Bytes::from(json_body));
let request = Request::builder()
.body(BufferedBody::Passthrough(body))
.unwrap();
let request = CacheableHttpRequest::from_request(request);
let filter = JqExpression::compile(".field").unwrap();
let predicate = NeutralRequestPredicate::new().body(Operation::Jq {
filter,
operation: JqOperation::Eq("wrong-value".into()),
});
let prediction = predicate.check(request).await;
assert!(matches!(prediction, PredicateResult::NonCacheable(_)));
}
#[tokio::test]
async fn test_field_not_found() {
let json_body = r#"{"field":"test-value"}"#;
let body = Full::new(Bytes::from(json_body));
let request = Request::builder()
.body(BufferedBody::Passthrough(body))
.unwrap();
let request = CacheableHttpRequest::from_request(request);
let predicate = NeutralRequestPredicate::new().body(Operation::Jq {
filter: JqExpression::compile(".wrong_field").unwrap(),
operation: JqOperation::Eq("test-value".into()),
});
let prediction = predicate.check(request).await;
assert!(matches!(prediction, PredicateResult::NonCacheable(_)));
}
}
#[cfg(test)]
mod exist_tests {
use super::*;
use http_body_util::Full;
#[tokio::test]
async fn test_positive() {
let json_body = r#"{"field":"test-value"}"#;
let body = Full::new(Bytes::from(json_body));
let request = Request::builder()
.body(BufferedBody::Passthrough(body))
.unwrap();
let request = CacheableHttpRequest::from_request(request);
let predicate = NeutralRequestPredicate::new().body(Operation::Jq {
filter: JqExpression::compile(".field").unwrap(),
operation: JqOperation::Exist,
});
let prediction = predicate.check(request).await;
assert!(matches!(prediction, PredicateResult::Cacheable(_)));
}
#[tokio::test]
async fn test_negative() {
let json_body = r#"{"other_field":"test-value"}"#;
let body = Full::new(Bytes::from(json_body));
let request = Request::builder()
.body(BufferedBody::Passthrough(body))
.unwrap();
let request = CacheableHttpRequest::from_request(request);
let predicate = NeutralRequestPredicate::new().body(Operation::Jq {
filter: JqExpression::compile(".field").unwrap(),
operation: JqOperation::Exist,
});
let prediction = predicate.check(request).await;
assert!(matches!(prediction, PredicateResult::NonCacheable(_)));
}
}
#[cfg(test)]
mod in_tests {
use super::*;
use http_body_util::Full;
#[tokio::test]
async fn test_positive() {
let json_body = r#"{"field":"test-value"}"#;
let body = Full::new(Bytes::from(json_body));
let request = Request::builder()
.body(BufferedBody::Passthrough(body))
.unwrap();
let request = CacheableHttpRequest::from_request(request);
let values = vec!["value-1".to_owned(), "test-value".to_owned()];
let predicate = NeutralRequestPredicate::new().body(Operation::Jq {
filter: JqExpression::compile(".field").unwrap(),
operation: JqOperation::In(values.into_iter().map(|v| v.into()).collect()),
});
let prediction = predicate.check(request).await;
assert!(matches!(prediction, PredicateResult::Cacheable(_)));
}
#[tokio::test]
async fn test_negative() {
let json_body = r#"{"field":"wrong-value"}"#;
let body = Full::new(Bytes::from(json_body));
let request = Request::builder()
.body(BufferedBody::Passthrough(body))
.unwrap();
let request = CacheableHttpRequest::from_request(request);
let values = vec!["value-1".to_owned(), "test-value".to_owned()];
let predicate = NeutralRequestPredicate::new().body(Operation::Jq {
filter: JqExpression::compile(".field").unwrap(),
operation: JqOperation::In(values.into_iter().map(|v| v.into()).collect()),
});
let prediction = predicate.check(request).await;
assert!(matches!(prediction, PredicateResult::NonCacheable(_)));
}
}
#[tokio::test]
async fn test_request_body_predicates_positive_basic() {
let json_body = r#"{"inner":{"field_one":"value_one","field_two":"value_two"}}"#;
let body = http_body_util::Full::new(Bytes::from(json_body));
let request = CacheableHttpRequest::from_request(
Request::builder()
.body(BufferedBody::Passthrough(body))
.unwrap(),
);
let predicate = NeutralRequestPredicate::new().body(Operation::Jq {
filter: JqExpression::compile(".inner.field_one").unwrap(),
operation: JqOperation::Eq("value_one".into()),
});
let prediction = predicate.check(request).await;
assert!(matches!(prediction, PredicateResult::Cacheable(_)));
}
#[tokio::test]
async fn test_request_body_predicates_positive_array() {
let json_body = r#"
[
{"key": "my-key-00", "value": "my-value-00"},
{"key": "my-key-01", "value": "my-value-01"}
]"#;
let body = http_body_util::Full::new(Bytes::from(json_body));
let request = CacheableHttpRequest::from_request(
Request::builder()
.body(BufferedBody::Passthrough(body))
.unwrap(),
);
let predicate = NeutralRequestPredicate::new().body(Operation::Jq {
filter: JqExpression::compile(".[1].key").unwrap(),
operation: JqOperation::Eq("my-key-01".into()),
});
let prediction = predicate.check(request).await;
assert!(matches!(prediction, PredicateResult::Cacheable(_)));
}
#[tokio::test]
async fn test_request_body_predicates_positive_multiple_value() {
let json_body = r#"
[
{"key": "my-key-00", "value": "my-value-00"},
{"key": "my-key-01", "value": "my-value-01"},
{"key": "my-key-02", "value": "my-value-02"}
]"#;
let body = http_body_util::Full::new(Bytes::from(json_body));
let request = CacheableHttpRequest::from_request(
Request::builder()
.body(BufferedBody::Passthrough(body))
.unwrap(),
);
let predicate = NeutralRequestPredicate::new().body(Operation::Jq {
filter: JqExpression::compile(".[].key").unwrap(),
operation: JqOperation::Eq(json!(["my-key-00", "my-key-01", "my-key-02"])),
});
let prediction = predicate.check(request).await;
assert!(matches!(prediction, PredicateResult::Cacheable(_)));
}
#[cfg(test)]
mod protobuf_tests {
}
#[cfg(test)]
mod buffered_body_tests {
use bytes::Bytes;
use futures::stream;
use hitbox_http::BufferedBody;
use http_body::Body;
use http_body_util::{BodyExt, Full, StreamBody};
#[tokio::test]
async fn test_complete_yields_bytes_once() {
let data = Bytes::from("hello world");
let mut body: BufferedBody<Full<Bytes>> = BufferedBody::Complete(Some(data.clone()));
let frame = body.frame().await.unwrap().unwrap();
let frame_data = frame.into_data().unwrap();
assert_eq!(frame_data, data);
let frame = body.frame().await;
assert!(frame.is_none());
}
#[tokio::test]
async fn test_passthrough_forwards_all_chunks() {
let data = Bytes::from("passthrough data");
let inner_body = Full::new(data.clone());
let mut body = BufferedBody::Passthrough(inner_body);
let frame = body.frame().await.unwrap().unwrap();
let frame_data = frame.into_data().unwrap();
assert_eq!(frame_data, data);
let frame = body.frame().await;
assert!(frame.is_none());
}
#[tokio::test]
async fn test_passthrough_with_stream_body() {
use std::convert::Infallible;
let stream = stream::iter(vec![
Ok::<_, Infallible>(http_body::Frame::data(Bytes::from("chunk1"))),
Ok::<_, Infallible>(http_body::Frame::data(Bytes::from("chunk2"))),
Ok::<_, Infallible>(http_body::Frame::data(Bytes::from("chunk3"))),
]);
let inner_body = StreamBody::new(stream);
let mut body = BufferedBody::Passthrough(inner_body);
let mut collected = Vec::new();
while let Some(result) = body.frame().await {
let frame = result.unwrap();
if let Ok(data) = frame.into_data() {
collected.push(data);
}
}
assert_eq!(collected.len(), 3);
assert_eq!(collected[0], Bytes::from("chunk1"));
assert_eq!(collected[1], Bytes::from("chunk2"));
assert_eq!(collected[2], Bytes::from("chunk3"));
}
#[tokio::test]
async fn test_passthrough_with_error_in_stream() {
use std::io;
let stream = stream::iter(vec![
Ok(http_body::Frame::data(Bytes::from("chunk1"))),
Ok(http_body::Frame::data(Bytes::from("chunk2"))),
Err(io::Error::new(
io::ErrorKind::ConnectionReset,
"connection reset",
)),
]);
let inner_body = StreamBody::new(stream);
let mut body = BufferedBody::Passthrough(inner_body);
let frame = body.frame().await.unwrap().unwrap();
let frame_data = frame.into_data().unwrap();
assert_eq!(frame_data, Bytes::from("chunk1"));
let frame = body.frame().await.unwrap().unwrap();
let frame_data = frame.into_data().unwrap();
assert_eq!(frame_data, Bytes::from("chunk2"));
let result = body.frame().await.unwrap();
assert!(result.is_err());
let frame = body.frame().await;
assert!(frame.is_none());
}
#[tokio::test]
async fn test_partial_yields_prefix_then_remaining() {
let _prefix = Bytes::from("prefix-");
use std::convert::Infallible;
let stream = stream::iter(vec![
Ok::<_, Infallible>(http_body::Frame::data(Bytes::from("chunk1"))),
Ok::<_, Infallible>(http_body::Frame::data(Bytes::from("chunk2"))),
]);
let remaining_body = StreamBody::new(stream);
let mut body = BufferedBody::Passthrough(remaining_body);
let frame = body.frame().await.unwrap().unwrap();
let frame_data = frame.into_data().unwrap();
assert_eq!(frame_data, Bytes::from("chunk1"));
let frame = body.frame().await.unwrap().unwrap();
let frame_data = frame.into_data().unwrap();
assert_eq!(frame_data, Bytes::from("chunk2"));
let frame = body.frame().await;
assert!(frame.is_none());
}
#[tokio::test]
async fn test_partial_with_stream_and_error() {
use std::io;
let stream = stream::iter(vec![
Ok(http_body::Frame::data(Bytes::from("remaining1"))),
Err(io::Error::new(io::ErrorKind::BrokenPipe, "broken pipe")),
]);
let remaining_body = StreamBody::new(stream);
let mut body = BufferedBody::Passthrough(remaining_body);
let frame = body.frame().await.unwrap().unwrap();
let frame_data = frame.into_data().unwrap();
assert_eq!(frame_data, Bytes::from("remaining1"));
let result = body.frame().await.unwrap();
assert!(result.is_err());
let frame = body.frame().await;
assert!(frame.is_none());
}
#[tokio::test]
async fn test_size_hint_complete() {
let data = Bytes::from("hello");
let body: BufferedBody<Full<Bytes>> = BufferedBody::Complete(Some(data.clone()));
let hint = body.size_hint();
assert_eq!(hint.lower(), data.len() as u64);
assert_eq!(hint.upper(), Some(data.len() as u64));
}
#[tokio::test]
async fn test_size_hint_complete_after_consumed() {
let body = BufferedBody::<Full<Bytes>>::Complete(None);
let hint = body.size_hint();
assert_eq!(hint.lower(), 0);
assert_eq!(hint.upper(), Some(0));
}
#[tokio::test]
async fn test_size_hint_passthrough() {
let data = Bytes::from("hello");
let inner_body = Full::new(data.clone());
let body = BufferedBody::Passthrough(inner_body);
let hint = body.size_hint();
assert_eq!(hint.lower(), data.len() as u64);
assert_eq!(hint.upper(), Some(data.len() as u64));
}
#[tokio::test]
async fn test_is_end_stream_complete() {
let data = Bytes::from("hello");
let body: BufferedBody<Full<Bytes>> = BufferedBody::Complete(Some(data));
assert!(!body.is_end_stream());
let body = BufferedBody::<Full<Bytes>>::Complete(None);
assert!(body.is_end_stream());
}
#[tokio::test]
async fn test_is_end_stream_passthrough() {
let data = Bytes::from("hello");
let inner_body = Full::new(data);
let body = BufferedBody::Passthrough(inner_body);
assert!(!body.is_end_stream());
}
}