v0.25.1
This commit is contained in:
1 parent
87e571c170
commit
fa7abc9e53
308 files changed
+49746
-50237
No files matched your search
@@ -0,0 +1,395 @@
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn identity_decode_borrows_the_collected_body() {
|
||||
let body = b"const value = 1;";
|
||||
let decoded = decode(body, None, 1024).unwrap();
|
||||
assert!(matches!(decoded, Cow::Borrowed(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transform_memory_budget_accounts_for_identity_and_decoded_buffers() {
|
||||
assert_eq!(
|
||||
transform_memory_required_bytes(2 * 1024 * 1024, None, 2 * 1024 * 1024),
|
||||
Some(4 * 1024 * 1024)
|
||||
);
|
||||
assert_eq!(
|
||||
transform_memory_required_bytes(
|
||||
2 * 1024 * 1024,
|
||||
Some("gzip"),
|
||||
2 * 1024 * 1024
|
||||
),
|
||||
Some(6 * 1024 * 1024)
|
||||
);
|
||||
assert_eq!(
|
||||
transform_memory_required_bytes(
|
||||
DEFAULT_MEMORY_BUDGET_BYTES,
|
||||
Some("gzip"),
|
||||
DEFAULT_MEMORY_BUDGET_BYTES
|
||||
),
|
||||
Some(3 * DEFAULT_MEMORY_BUDGET_BYTES)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transform_memory_reservation_obeys_runtime_budget() {
|
||||
let counter = AtomicUsize::new(0);
|
||||
let required = 4 * 1024 * 1024;
|
||||
let reservation = try_reserve_memory_from(&counter, required, required).unwrap();
|
||||
assert!(try_reserve_memory_from(&counter, required, required).is_none());
|
||||
drop(reservation);
|
||||
assert!(try_reserve_memory_from(&counter, required, required).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transform_inflight_reservation_obeys_runtime_limit() {
|
||||
let counter = AtomicUsize::new(0);
|
||||
let slot = try_reserve_slot_from(&counter, 1).unwrap();
|
||||
assert!(try_reserve_slot_from(&counter, 1).is_none());
|
||||
drop(slot);
|
||||
assert!(try_reserve_slot_from(&counter, 1).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resource_usage_exposes_configured_limits() {
|
||||
let settings = Settings {
|
||||
memory_budget_bytes: 512 * 1024 * 1024,
|
||||
max_inflight: 64,
|
||||
..Default::default()
|
||||
};
|
||||
let usage = resource_usage(&settings);
|
||||
assert_eq!(usage.memory_budget_bytes, 512 * 1024 * 1024);
|
||||
assert_eq!(usage.max_inflight, 64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resource_preset_metadata_round_trips() {
|
||||
let mut settings = Settings::default();
|
||||
settings.preset = Some(Preset::Uber);
|
||||
settings.memory_budget_bytes = 1024 * 1024 * 1024;
|
||||
settings.max_body_bytes = 32 * 1024 * 1024;
|
||||
settings.max_inflight = 128;
|
||||
let encoded = serde_json::to_value(&settings).unwrap();
|
||||
assert_eq!(encoded["preset"], "uber");
|
||||
let decoded: Settings = serde_json::from_value(encoded).unwrap();
|
||||
assert_eq!(decoded.preset, Some(Preset::Uber));
|
||||
assert_eq!(decoded.memory_budget_bytes, 1024 * 1024 * 1024);
|
||||
assert_eq!(decoded.max_inflight, 128);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_transform_settings_receive_resource_defaults() {
|
||||
let decoded: Settings = serde_json::from_str(r#"{"enabled":true}"#).unwrap();
|
||||
assert!(decoded.enabled);
|
||||
assert_eq!(decoded.preset, None);
|
||||
assert_eq!(decoded.memory_budget_bytes, DEFAULT_MEMORY_BUDGET_BYTES);
|
||||
assert_eq!(decoded.max_body_bytes, DEFAULT_MAX_BODY_BYTES);
|
||||
assert_eq!(decoded.max_inflight, DEFAULT_MAX_INFLIGHT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bounded_decoder_rejects_expansion_past_limit() {
|
||||
use std::io::Write as _;
|
||||
let identity = vec![b'x'; 128 * 1024];
|
||||
let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
|
||||
encoder.write_all(&identity).unwrap();
|
||||
let encoded = encoder.finish().unwrap();
|
||||
assert!(decode(&encoded, Some("gzip"), 64 * 1024).is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn aggregation_is_bounded_even_when_content_length_is_false() {
|
||||
let settings = Settings {
|
||||
enabled: true,
|
||||
json: true,
|
||||
max_body_bytes: 1024,
|
||||
..Default::default()
|
||||
};
|
||||
let mut response = Response::new(Body::from(vec![b' '; 2048]));
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("content-type", HeaderValue::from_static("application/json"));
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("content-length", HeaderValue::from_static("128"));
|
||||
assert!(apply(response, "/data.json", &settings, false).await.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metrics_track_applied_skipped_and_cached_transformed_responses() {
|
||||
let metrics = Metrics::default();
|
||||
let mut applied = Response::new(Body::empty());
|
||||
applied.extensions_mut().insert(Applied {
|
||||
kind: Kind::Css,
|
||||
original_bytes: 100,
|
||||
transformed_bytes: 70,
|
||||
});
|
||||
metrics.record_response("app", &applied, Duration::from_micros(500));
|
||||
let mut skipped = Response::new(Body::empty());
|
||||
skipped.extensions_mut().insert(Skipped {
|
||||
kind: Some(Kind::Css),
|
||||
reason: "response_no_transform",
|
||||
});
|
||||
metrics.record_response("app", &skipped, Duration::from_micros(100));
|
||||
metrics.record_cache_hit("app");
|
||||
let total = metrics.total();
|
||||
assert_eq!(total.applied, 1);
|
||||
assert_eq!(total.cache_hits, 1);
|
||||
let view = metrics.view();
|
||||
assert_eq!(view.total.examined, 2);
|
||||
assert_eq!(view.total.applied, 1);
|
||||
assert_eq!(view.total.skipped, 1);
|
||||
assert_eq!(view.total.cache_hits, 1);
|
||||
assert_eq!(view.total.saved_bytes, 30);
|
||||
assert_eq!(view.total.by_kind.get("css"), Some(&1));
|
||||
assert_eq!(
|
||||
view.total.reasons.get("response_no_transform"),
|
||||
Some(&1)
|
||||
);
|
||||
assert_eq!(view.scopes.len(), 1);
|
||||
assert_eq!(view.scopes[0].vhost, "app");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn per_vhost_is_opt_in_even_when_global_is_enabled() {
|
||||
let global = Settings {
|
||||
enabled: true,
|
||||
..Default::default()
|
||||
};
|
||||
assert!(!Overrides::default().resolve(&global).unwrap().enabled);
|
||||
let enabled = Overrides {
|
||||
enabled: true,
|
||||
..Default::default()
|
||||
};
|
||||
assert!(enabled.resolve(&global).unwrap().enabled);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn json_is_compacted_and_gets_a_new_validator() {
|
||||
let settings = Settings {
|
||||
enabled: true,
|
||||
json: true,
|
||||
..Default::default()
|
||||
};
|
||||
let mut response = Response::new(Body::from("{ \"a\": 1, \"b\": [ true ] }"));
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("content-type", HeaderValue::from_static("application/json"));
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("content-length", HeaderValue::from_static("30"));
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("etag", HeaderValue::from_static("\"origin\""));
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("vary", HeaderValue::from_static("Accept-Encoding"));
|
||||
let response = apply(response, "/data.json", &settings, false).await.unwrap();
|
||||
assert!(response.headers()["etag"].to_str().unwrap().starts_with("\"rustproxy-"));
|
||||
assert!(!response.headers().contains_key("vary"));
|
||||
assert!(response.extensions().get::<CanonicalIdentity>().is_some());
|
||||
let validators = response.extensions().get::<OriginValidators>().unwrap();
|
||||
assert_eq!(validators.etag.as_ref().unwrap(), "\"origin\"");
|
||||
let body = response.into_body().collect().await.unwrap().to_bytes();
|
||||
assert_eq!(&body[..], br#"{"a":1,"b":[true]}"#);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unchanged_body_keeps_origin_validator() {
|
||||
let settings = Settings {
|
||||
enabled: true,
|
||||
json: true,
|
||||
..Default::default()
|
||||
};
|
||||
let body = br#"{"a":1}"#;
|
||||
let mut response = Response::new(Body::from(body.as_slice()));
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("content-type", HeaderValue::from_static("application/json"));
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("content-length", HeaderValue::from_static("7"));
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("etag", HeaderValue::from_static("\"origin\""));
|
||||
response.headers_mut().insert(
|
||||
"vary",
|
||||
HeaderValue::from_static("Accept-Encoding, X-Theme"),
|
||||
);
|
||||
let response = apply(response, "/data.json", &settings, false).await.unwrap();
|
||||
assert_eq!(response.headers()["etag"], "\"origin\"");
|
||||
assert_eq!(response.headers()["vary"], "X-Theme");
|
||||
assert!(response.extensions().get::<CanonicalIdentity>().is_some());
|
||||
assert!(response.extensions().get::<Applied>().is_none());
|
||||
}
|
||||
#[tokio::test]
|
||||
async fn unchanged_compressed_body_is_canonicalized_to_identity() {
|
||||
use std::io::Write as _;
|
||||
|
||||
let settings = Settings {
|
||||
enabled: true,
|
||||
json: true,
|
||||
..Default::default()
|
||||
};
|
||||
let identity = br#"{"a":1}"#;
|
||||
let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
|
||||
encoder.write_all(identity).unwrap();
|
||||
let encoded = encoder.finish().unwrap();
|
||||
|
||||
let mut response = Response::new(Body::from(encoded.clone()));
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("content-type", HeaderValue::from_static("application/json"));
|
||||
response.headers_mut().insert(
|
||||
"content-length",
|
||||
HeaderValue::from_str(&encoded.len().to_string()).unwrap(),
|
||||
);
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("content-encoding", HeaderValue::from_static("gzip"));
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("etag", HeaderValue::from_static("\"origin\""));
|
||||
|
||||
let response = apply(response, "/data.json", &settings, false).await.unwrap();
|
||||
assert!(!response.headers().contains_key("content-encoding"));
|
||||
assert_eq!(
|
||||
response.headers()["content-length"].to_str().unwrap(),
|
||||
identity.len().to_string()
|
||||
);
|
||||
assert!(response.headers()["etag"].to_str().unwrap().starts_with("\"rustproxy-"));
|
||||
let validators = response.extensions().get::<OriginValidators>().unwrap();
|
||||
assert_eq!(validators.etag.as_ref().unwrap(), "\"origin\"");
|
||||
let applied = response.extensions().get::<Applied>().unwrap();
|
||||
assert_eq!(applied.original_bytes, identity.len());
|
||||
assert_eq!(applied.transformed_bytes, identity.len());
|
||||
let body = response.into_body().collect().await.unwrap().to_bytes();
|
||||
assert_eq!(&body[..], identity);
|
||||
}
|
||||
#[tokio::test]
|
||||
async fn origin_no_transform_blocks_body_changes_and_keeps_validator() {
|
||||
let settings = Settings {
|
||||
enabled: true,
|
||||
json: true,
|
||||
..Default::default()
|
||||
};
|
||||
let body = br#"{ "a": 1 }"#;
|
||||
let mut response = Response::new(Body::from(body.as_slice()));
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("content-type", HeaderValue::from_static("application/json"));
|
||||
response.headers_mut().insert(
|
||||
"content-length",
|
||||
HeaderValue::from_str(&body.len().to_string()).unwrap(),
|
||||
);
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("etag", HeaderValue::from_static("\"origin\""));
|
||||
|
||||
let response = apply(response, "/data.json", &settings, true).await.unwrap();
|
||||
assert_eq!(response.headers()["etag"], "\"origin\"");
|
||||
assert!(response.extensions().get::<Applied>().is_none());
|
||||
let received = response.into_body().collect().await.unwrap().to_bytes();
|
||||
assert_eq!(&received[..], body);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn final_no_transform_blocks_body_changes() {
|
||||
let settings = Settings {
|
||||
enabled: true,
|
||||
json: true,
|
||||
..Default::default()
|
||||
};
|
||||
let body = br#"{ "a": 1 }"#;
|
||||
let mut response = Response::new(Body::from(body.as_slice()));
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("content-type", HeaderValue::from_static("application/json"));
|
||||
response.headers_mut().insert(
|
||||
"content-length",
|
||||
HeaderValue::from_str(&body.len().to_string()).unwrap(),
|
||||
);
|
||||
response.headers_mut().insert(
|
||||
"cache-control",
|
||||
HeaderValue::from_static("public, max-age=60, no-transform"),
|
||||
);
|
||||
|
||||
let response = apply(response, "/data.json", &settings, false).await.unwrap();
|
||||
assert!(response.extensions().get::<Applied>().is_none());
|
||||
let received = response.into_body().collect().await.unwrap().to_bytes();
|
||||
assert_eq!(&received[..], body);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn empty_head_body_with_origin_length_keeps_origin_representation_headers() {
|
||||
let settings = Settings {
|
||||
enabled: true,
|
||||
json: true,
|
||||
..Default::default()
|
||||
};
|
||||
let mut response = Response::new(Body::empty());
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("content-type", HeaderValue::from_static("application/json"));
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("content-length", HeaderValue::from_static("76"));
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("etag", HeaderValue::from_static("\"origin-head\""));
|
||||
|
||||
let response = apply(response, "/data.json", &settings, false).await.unwrap();
|
||||
assert_eq!(response.headers()["content-length"], "76");
|
||||
assert_eq!(response.headers()["etag"], "\"origin-head\"");
|
||||
assert!(response.extensions().get::<Applied>().is_none());
|
||||
assert!(response.extensions().get::<CanonicalIdentity>().is_none());
|
||||
let body = response.into_body().collect().await.unwrap().to_bytes();
|
||||
assert!(body.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn compressed_no_transform_preserves_origin_encoding_and_bytes() {
|
||||
use std::io::Write as _;
|
||||
|
||||
let settings = Settings {
|
||||
enabled: true,
|
||||
json: true,
|
||||
..Default::default()
|
||||
};
|
||||
let identity = br#"{ "a": 1 }"#;
|
||||
let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
|
||||
encoder.write_all(identity).unwrap();
|
||||
let encoded = encoder.finish().unwrap();
|
||||
let mut response = Response::new(Body::from(encoded.clone()));
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("content-type", HeaderValue::from_static("application/json"));
|
||||
response.headers_mut().insert(
|
||||
"content-length",
|
||||
HeaderValue::from_str(&encoded.len().to_string()).unwrap(),
|
||||
);
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("content-encoding", HeaderValue::from_static("gzip"));
|
||||
response.headers_mut().insert(
|
||||
"cache-control",
|
||||
HeaderValue::from_static("public, max-age=60, no-transform"),
|
||||
);
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("vary", HeaderValue::from_static("Accept-Encoding"));
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("etag", HeaderValue::from_static("\"origin\""));
|
||||
|
||||
let response = apply(response, "/data.json", &settings, false).await.unwrap();
|
||||
assert_eq!(response.headers()["content-encoding"], "gzip");
|
||||
assert_eq!(response.headers()["etag"], "\"origin\"");
|
||||
assert_eq!(response.headers()["vary"], "Accept-Encoding");
|
||||
assert!(response.extensions().get::<CanonicalIdentity>().is_none());
|
||||
let received = response.into_body().collect().await.unwrap().to_bytes();
|
||||
assert_eq!(&received[..], encoded.as_slice());
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user