use esi::{Configuration, Processor};
use fastly::{Error, Request};
use log::debug;
use std::sync::Once;
static INIT: Once = Once::new();
pub fn init_logs() {
INIT.call_once(|| {
let default = format!("warn,{}=debug", env!("CARGO_CRATE_NAME"));
env_logger::Builder::from_env(env_logger::Env::default().filter_or("RUST_LOG", &default))
.is_test(true) .init();
log::debug!("debug is enabled)");
});
}
fn process_esi_document(input: &str, req: Request) -> Result<String, Error> {
debug!("Processing ESI document: {input:?}");
let reader = esi::Reader::from_str(input);
let buffer = Vec::new();
let cursor = std::io::Cursor::new(buffer);
let mut writer = esi::Writer::new(cursor);
let processor = Processor::new(Some(req), Configuration::default());
processor.process_document(reader, &mut writer, None, None)?;
let output_buffer = writer.into_inner().into_inner();
let result = String::from_utf8(output_buffer)
.map_err(|e| Error::msg(format!("Invalid UTF-8 in processed output: {e}")))?;
debug!("Processed result: {result:?}");
Ok(result)
}
#[test]
fn test_bareword_subfield_query_string() {
init_logs();
let input = r#"
<esi:vars>
$(QUERY_STRING{param})
</esi:vars>
"#;
let req = Request::get("http://example.com?param=value");
let result = process_esi_document(input, req).expect("Processing should succeed");
assert_eq!(
result.trim(),
"value",
"Bareword subfield should resolve to 'value'"
);
}
#[test]
fn test_bareword_function_argument_is_swallowed() {
let input = r#"
<esi:vars>
$lower(bareword)
</esi:vars>
"#;
let req = Request::get("http://example.com");
let result = process_esi_document(input, req)
.expect("ESI processing should succeed; interpolation errors are intentionally swallowed");
assert!(
result.trim().is_empty(),
"Expected empty output when a bareword is used as a function argument during interpolation, got: {:?}",
result
);
}
#[test]
fn test_mixed_subfield_types() {
let input = r#"
<esi:assign name="keyVar" value="'param'" />
<esi:vars>
$(QUERY_STRING{param})
$(QUERY_STRING{$(keyVar)})
</esi:vars>
"#;
let req = Request::get("http://example.com?param=value");
let result = process_esi_document(input, req).expect("Processing should succeed");
assert_eq!(
result.trim(),
"value\n value",
"Bareword and expression subfields should both resolve to 'value'"
);
}
#[test]
fn test_esi_choose_compatibility_equal() {
let input = r#"
<esi:choose>
<esi:when test="$(QUERY_STRING{param}) == 'value'">
Match
</esi:when>
<esi:otherwise>
Fallback
</esi:otherwise>
</esi:choose>
"#;
let req = Request::get("http://example.com?param=value");
let result = process_esi_document(input, req).expect("Processing should succeed");
assert_eq!(
result.trim(),
"Match",
"ESI choose/when should work with bareword subfield"
);
}
#[test]
fn test_esi_choose_compatibility_not_equal() {
let input = r#"
<esi:choose>
<esi:when test="$(QUERY_STRING{param}) != 'wrongvalue'">
Match
</esi:when>
<esi:otherwise>
Fallback
</esi:otherwise>
</esi:choose>
"#;
let req = Request::get("http://example.com?param=value");
let result = process_esi_document(input, req).expect("Processing should succeed");
assert_eq!(
result.trim(),
"Match",
"ESI choose/when should work with bareword subfield"
);
}
#[test]
fn test_nested_subfields() {
let input = r#"
<esi:assign name="outer" value="'QUERY_STRING'" />
<esi:vars>
$($(outer){param})
</esi:vars>
"#;
let req = Request::get("http://example.com?param=value");
let result = process_esi_document(input, req).expect("Processing should succeed");
assert_ne!(
result.trim(),
"value",
"Nested variable resolution should not work"
);
}
#[test]
fn process_include_with_query_string_interpolation() -> Result<(), Error> {
use esi::{Configuration, Processor};
use fastly::{Request, Response};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
let esi_document = r#"<esi:include
src="/v1/product?apiKey=$(QUERY_STRING{apiKey})" />"#;
let req = Some(Request::get("http://example.com?apiKey=value"));
let mut resp = Response::from_body(esi_document);
let processor = Processor::new(req, Configuration::default());
let correct_fragment_request_made = Arc::new(AtomicBool::new(false));
let correct_fragment_request_made_clone = Arc::clone(&correct_fragment_request_made);
processor
.process_response(
&mut resp,
None,
Some(&move |fragment_req: Request| {
let url = fragment_req.get_url();
let contains_api_key = url.to_string().contains("apiKey=value");
correct_fragment_request_made_clone.store(contains_api_key, Ordering::SeqCst);
Ok(esi::PendingFragmentContent::CompletedRequest(
Response::from_body("fragment content"),
))
}),
None,
)
.unwrap();
assert!(
correct_fragment_request_made.load(Ordering::SeqCst),
"Fragment request should contain the interpolated apiKey value"
);
Ok(())
}
#[test]
fn test_simple_negation() {
let input = r#"
<esi:choose>
<esi:when test="!$(QUERY_STRING{empty})">
Empty parameter was negated
</esi:when>
<esi:otherwise>
Fallback
</esi:otherwise>
</esi:choose>
"#;
let req = Request::get("http://example.com?nonempty=value");
let result = process_esi_document(input, req).expect("Processing should succeed");
assert_eq!(
result.trim(),
"Empty parameter was negated",
"Negation of null/empty value should evaluate to true"
);
}
#[test]
fn test_negation_with_value() {
let input = r#"
<esi:choose>
<esi:when test="!$(QUERY_STRING{param})">
Parameter was negated
</esi:when>
<esi:otherwise>
Parameter exists
</esi:otherwise>
</esi:choose>
"#;
let req = Request::get("http://example.com?param=value");
let result = process_esi_document(input, req).expect("Processing should succeed");
assert_eq!(
result.trim(),
"Parameter exists",
"Negation of non-empty value should evaluate to false"
);
}
#[test]
fn test_negation_of_comparison() {
let input = r#"
<esi:choose>
<esi:when test="!($(QUERY_STRING{param}) == 'wrong')">
Comparison was negated
</esi:when>
<esi:otherwise>
Fallback
</esi:otherwise>
</esi:choose>
"#;
let req = Request::get("http://example.com?param=value");
let result = process_esi_document(input, req).expect("Processing should succeed");
assert_eq!(
result.trim(),
"Comparison was negated",
"Negation of false comparison should evaluate to true"
);
}
#[test]
fn test_double_negation() {
let input = r#"
<esi:choose>
<esi:when test="!!$(QUERY_STRING{param})">
Double negation works
</esi:when>
<esi:otherwise>
Fallback
</esi:otherwise>
</esi:choose>
"#;
let req = Request::get("http://example.com?param=value");
let result = process_esi_document(input, req).expect("Processing should succeed");
assert_eq!(
result.trim(),
"Double negation works",
"Double negation should restore original boolean value"
);
}
#[test]
fn test_negation_with_not_equals() {
let input = r#"
<esi:choose>
<esi:when test="!($(QUERY_STRING{param}) != 'value')">
Negation of not-equals works
</esi:when>
<esi:otherwise>
Fallback
</esi:otherwise>
</esi:choose>
"#;
let req = Request::get("http://example.com?param=value");
let result = process_esi_document(input, req).expect("Processing should succeed");
assert_eq!(
result.trim(),
"Negation of not-equals works",
"Negation of not-equals should work correctly"
);
}
#[test]
fn test_negation_in_vars() {
let input = r#"
<esi:vars>
<esi:assign name="result" value="!$(QUERY_STRING{empty})" />
$(result)
</esi:vars>
"#;
let req = Request::get("http://example.com?nonempty=value");
let result = process_esi_document(input, req).expect("Processing should succeed");
assert_eq!(
result.trim(),
"true",
"Negation in variable assignment should work"
);
}